MVCC-3. Versões de linha

Então, analisamos questões relacionadas ao isolamento e fizemos uma digressão sobre a organização de dados em um nível baixo . E finalmente chegou ao mais interessante - à versão das linhas.

Manchete


Como já dissemos, cada linha pode estar presente simultaneamente no banco de dados em várias versões. Uma versão deve ser distinguida da outra de alguma forma.Para esse efeito, cada versão possui duas marcas que determinam o "tempo" da ação desta versão (xmin e xmax). Entre aspas - porque não é o tempo usado como tal, mas um contador incremental especial. E esse contador é o número da transação.

(Como sempre, é realmente mais complicado: o número da transação não pode aumentar o tempo todo devido à capacidade limitada de bits do contador. Mas consideraremos esses detalhes em detalhes quando chegarmos ao congelamento.)

Quando a linha é criada, xmin é definido como o número da transação que executou o comando INSERT e xmax não é preenchido.

Quando uma linha é excluída, o valor xmax da versão atual é marcado com o número da transação que executou DELETE.

Quando uma linha é modificada com o comando UPDATE, duas operações são realmente executadas: DELETE e INSERT. Na versão atual da linha, xmax é definido igual ao número da transação que executou UPDATE. Em seguida, uma nova versão da mesma linha é criada; seu valor xmin corresponde ao valor xmax da versão anterior.

Os campos xmin e xmax estão incluídos no cabeçalho da versão da linha. Além desses campos, o cabeçalho contém outros, por exemplo:

  • infomask - uma série de bits que definem as propriedades desta versão. Existem muitos deles; os principais que consideraremos gradualmente.
  • ctid - um link para a próxima versão mais nova da mesma linha. Na versão mais recente e mais recente da cadeia, ctid refere-se a esta versão. O número tem o formato (x, y), onde x é o número da página, y é o número de série do ponteiro na matriz.
  • bitmap de valores indefinidos - marca as colunas desta versão que contêm um valor indefinido (NULL). NULL não é um dos valores usuais dos tipos de dados, portanto, o atributo deve ser armazenado separadamente.

Como resultado, o cabeçalho é bastante grande - pelo menos 23 bytes por versão da string e, geralmente, mais devido ao bitmap NULL. Se a tabela for "estreita" (ou seja, contém poucas colunas), a sobrecarga poderá levar mais do que informações úteis.

Inserir


Vamos dar uma olhada em como as operações de string são executadas em um nível baixo e começar com a inserção.

Para experimentos, crie uma nova tabela com duas colunas e um índice em uma delas:

=> CREATE TABLE t( id serial, s text ); => CREATE INDEX ON t(s); 

Insira uma linha, depois de iniciar a transação.

 => BEGIN; => INSERT INTO t(s) VALUES ('FOO'); 

Aqui está o número da nossa transação atual:

 => SELECT txid_current(); 
  txid_current -------------- 3664 (1 row) 

Dê uma olhada no conteúdo da página. A função heap_page_items da extensão pageinspect fornece informações sobre ponteiros e versões de linha:

 => SELECT * FROM heap_page_items(get_raw_page('t',0)) \gx 
 -[ RECORD 1 ]------------------- lp | 1 lp_off | 8160 lp_flags | 1 lp_len | 32 t_xmin | 3664 t_xmax | 0 t_field3 | 0 t_ctid | (0,1) t_infomask2 | 2 t_infomask | 2050 t_hoff | 24 t_bits | t_oid | t_data | \x0100000009464f4f 

Observe que a palavra heap (heap) no PostgreSQL se refere a tabelas. Esse é outro uso estranho do termo - heap é uma estrutura de dados conhecida que não tem nada a ver com uma tabela. Aqui, essa palavra é usada no sentido de "tudo é empilhado", em contraste com os índices ordenados.

A função exibe os dados "como estão" em um formato difícil de ler. Para entender, deixaremos apenas uma parte da informação e a descriptografaremos:

 => SELECT '(0,'||lp||')' 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 as xmin, t_xmax as xmax, (t_infomask & 256) > 0 AS xmin_commited, (t_infomask & 512) > 0 AS xmin_aborted, (t_infomask & 1024) > 0 AS xmax_commited, (t_infomask & 2048) > 0 AS xmax_aborted, t_ctid FROM heap_page_items(get_raw_page('t',0)) \gx 
 -[ RECORD 1 ]-+------- ctid | (0,1) state | normal xmin | 3664 xmax | 0 xmin_commited | f xmin_aborted | f xmax_commited | f xmax_aborted | t t_ctid | (0,1) 

Aqui está o que fizemos:

  • Adicionamos um zero ao número do índice para trazê-lo da mesma forma que t_ctid: (número da página, número do índice).
  • Descriptografou o estado do ponteiro lp_flags. Aqui está "normal" - isso significa que o ponteiro realmente se refere à versão da string. Outros valores serão considerados posteriormente.
  • De todos os bits de informação, até agora apenas dois pares foram alocados. Os bits xmin_committed e xmin_aborted indicam se a transação com o número xmin está confirmada (cancelada). Dois bits semelhantes se referem ao número de transação xmax.

O que nós vemos? Quando você insere uma linha na página da tabela, um ponteiro aparece com o número 1, referindo-se à primeira e única versão da linha.

Na versão da linha, o campo xmin é preenchido com o número da transação atual. A transação ainda está ativa, portanto, os bits xmin_committed e xmin_aborted não estão definidos.

O campo ctid da versão da linha se refere à mesma linha. Isso significa que não existe uma versão mais recente.

O campo xmax é preenchido com um número fictício 0, porque esta versão da linha não é excluída e é relevante. As transações não prestarão atenção a esse número, porque o bit xmax_aborted está definido.

Vamos dar mais um passo para melhorar a legibilidade adicionando bits de informação aos números das transações. E criaremos uma função, pois precisaremos da solicitação mais de uma vez:

 => CREATE FUNCTION heap_page(relname text, pageno integer) RETURNS TABLE(ctid tid, state text, xmin text, xmax 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, t_ctid FROM heap_page_items(get_raw_page(relname,pageno)) ORDER BY lp; $$ LANGUAGE SQL; 

Nesta forma, é muito mais claro o que está acontecendo no cabeçalho da versão da string:

 => SELECT * FROM heap_page('t',0); 
  ctid | state | xmin | xmax | t_ctid -------+--------+------+-------+-------- (0,1) | normal | 3664 | 0 (a) | (0,1) (1 row) 

Informações semelhantes, mas substancialmente menos detalhadas, podem ser obtidas da própria tabela, usando as pseudo-colunas xmin e xmax:

 => SELECT xmin, xmax, * FROM t; 
  xmin | xmax | id | s ------+------+----+----- 3664 | 0 | 1 | FOO (1 row) 

Fixação


Após a conclusão bem-sucedida da transação, você precisa se lembrar de seu status - observe que ela foi corrigida. Para fazer isso, use uma estrutura chamada XACT (e antes da versão 10 era CLOG (commit log) e esse nome ainda pode ser encontrado em locais diferentes).

XACT não é uma tabela de catálogo do sistema; Esses são os arquivos no diretório PGDATA / pg_xact. Neles, para cada transação, dois bits são alocados: confirmados e abortados - exatamente o mesmo que no cabeçalho da versão da linha. Essas informações são divididas em vários arquivos apenas por conveniência. Voltaremos a esse problema quando considerarmos o congelamento. E o trabalho com esses arquivos é realizado página por página, como em todos os outros.

Portanto, ao confirmar uma transação no XACT, o bit confirmado é definido para esta transação. E isso é tudo o que acontece durante o commit (embora ainda não falemos sobre o diário de pré-registro).

Quando qualquer outra transação acessa a página da tabela que acabamos de ver, ela terá que responder algumas perguntas.

  1. A transação xmin foi concluída? Caso contrário, a versão gerada da sequência não deverá estar visível.
    Essa verificação é realizada olhando para outra estrutura, que está localizada na memória compartilhada da instância e é chamada ProcArray. Ele contém uma lista de todos os processos ativos e para cada um é indicado o número de sua transação atual (ativa).
  2. Se concluído, como - por fixação ou cancelamento? Se cancelada, a versão da sequência também não deverá estar visível.
    É exatamente para isso que serve o XACT. Porém, embora as últimas páginas XACT sejam armazenadas em buffers na RAM, não é necessário verificar o XACT a cada vez. Portanto, o status de uma transação, uma vez esclarecido, é registrado nos bits xmin_committed e xmin_aborted da versão da linha. Se um desses bits for definido, o estado da transação xmin será considerado conhecido e a próxima transação não precisará mais acessar o XACT.

Por que esses bits não são definidos pela própria transação que realiza a inserção? Quando uma inserção ocorre, a transação ainda não sabe se será concluída com êxito. E, no momento da correção, já não está claro quais linhas em que páginas foram alteradas. Pode haver muitas páginas desse tipo, e memorizá-las é uma desvantagem. Além disso, parte das páginas pode ser enviada do cache do buffer para o disco; lê-los novamente para alterar os bits significaria uma desaceleração significativa do commit.

A desvantagem da economia é que, após as alterações, qualquer transação (mesmo executando uma simples leitura - SELECT) pode começar a alterar as páginas de dados no cache do buffer.

Então, conserte a mudança.

 => COMMIT; 

Nada mudou na página (mas sabemos que o status da transação já está registrado no XACT):

 => SELECT * FROM heap_page('t',0); 
  ctid | state | xmin | xmax | t_ctid -------+--------+------+-------+-------- (0,1) | normal | 3664 | 0 (a) | (0,1) (1 row) 

Agora a transação que acessa primeiro a página terá que determinar o status da transação xmin e gravá-la nos bits de informação:

 => SELECT * FROM t; 
  id | s ----+----- 1 | FOO (1 row) 

 => SELECT * FROM heap_page('t',0); 
  ctid | state | xmin | xmax | t_ctid -------+--------+----------+-------+-------- (0,1) | normal | 3664 (c) | 0 (a) | (0,1) (1 row) 

Excluir


Quando uma linha é excluída, o número da transação de exclusão atual é registrado no campo xmax da versão atual e o bit xmax_aborted é redefinido.

Observe que o valor xmax definido correspondente à transação ativa atua como um bloqueio de linha. Se outra transação estiver prestes a atualizar ou excluir esta linha, ela será forçada a aguardar a conclusão da transação xmax. Falaremos mais sobre bloqueios mais tarde. Por enquanto, apenas observamos que o número de bloqueios de linha é ilimitado. Eles não ocupam um lugar na RAM e o desempenho do sistema não sofre com a quantidade. É verdade que transações “longas” têm outras desvantagens, mas mais sobre isso mais tarde.

Exclua a linha.

 => BEGIN; => DELETE FROM t; => SELECT txid_current(); 
  txid_current -------------- 3665 (1 row) 

Vemos que o número da transação é registrado no campo xmax, mas os bits de informação não estão definidos:

 => SELECT * FROM heap_page('t',0); 
  ctid | state | xmin | xmax | t_ctid -------+--------+----------+------+-------- (0,1) | normal | 3664 (c) | 3665 | (0,1) (1 row) 

Cancelar


A reversão de alterações funciona de maneira semelhante à confirmação, apenas no XACT para a transação que o bit anulado está definido. O cancelamento é tão rápido quanto confirmar. Embora o comando seja chamado ROLLBACK, a alteração não é revertida: tudo o que a transação conseguiu alterar nas páginas de dados permanece inalterado.

 => ROLLBACK; => SELECT * FROM heap_page('t',0); 
  ctid | state | xmin | xmax | t_ctid -------+--------+----------+------+-------- (0,1) | normal | 3664 (c) | 3665 | (0,1) (1 row) 

Ao acessar a página, o status será verificado e o bit de dica xmax_aborted será definido na versão da linha. O próprio número xmax permanece na página, mas ninguém o verá.

 => SELECT * FROM t; 
  id | s ----+----- 1 | FOO (1 row) 

 => SELECT * FROM heap_page('t',0); 
  ctid | state | xmin | xmax | t_ctid -------+--------+----------+----------+-------- (0,1) | normal | 3664 (c) | 3665 (a) | (0,1) (1 row) 

Update


A atualização funciona como se estivesse primeiro excluindo a versão atual da linha e depois inserindo uma nova.

 => BEGIN; => UPDATE t SET s = 'BAR'; => SELECT txid_current(); 
  txid_current -------------- 3666 (1 row) 

A solicitação produz uma linha (nova versão):

 => SELECT * FROM t; 
  id | s ----+----- 1 | BAR (1 row) 

Mas na página vemos as duas versões:

 => SELECT * FROM heap_page('t',0); 
  ctid | state | xmin | xmax | t_ctid -------+--------+----------+-------+-------- (0,1) | normal | 3664 (c) | 3666 | (0,2) (0,2) | normal | 3666 | 0 (a) | (0,2) (2 rows) 

A versão remota é marcada com o número da transação atual no campo xmax. Além disso, esse valor é substituído pelo antigo, pois a transação anterior foi cancelada. E o bit xmax_aborted é redefinido, porque o status da transação atual ainda é desconhecido.

A primeira versão da linha agora se refere à segunda (campo t_ctid), como uma mais recente.

Um segundo ponteiro e uma segunda linha aparecem na página de índice, vinculados à segunda versão na página da tabela.

Como na exclusão, o valor xmax na primeira versão da sequência é um sinal de que a sequência está bloqueada.

Bem, complete a transação.

 => COMMIT; 

Índices


Até agora, falamos apenas de páginas tabulares. E o que acontece dentro dos índices?

As informações nas páginas de índice são altamente dependentes do tipo específico de índice. E mesmo um tipo de índice possui diferentes tipos de páginas. Por exemplo, a árvore B possui uma página com metadados e páginas "regulares".

No entanto, uma página geralmente possui uma matriz de ponteiros para as linhas e as próprias linhas (como em uma página da tabela). Além disso, no final da página, há um local para dados especiais.

Linhas em índices também podem ter uma estrutura muito diferente, dependendo do tipo de índice. Por exemplo, para uma árvore B, as linhas relacionadas às páginas folha contêm o valor da chave de índice e um link (ctid) para a linha correspondente da tabela. Em geral, um índice pode ser organizado de uma maneira completamente diferente.

O ponto mais importante é que não há versões de linha em nenhum tipo de índice. Bem, ou podemos assumir que cada linha é representada por exatamente uma versão. Em outras palavras, não há campos xmin e xmax no cabeçalho da linha do índice. Podemos assumir que os links do índice levam a todas as versões tabulares das linhas - portanto, você só pode descobrir qual versão a transação verá se olhar para a tabela. (Como sempre, essa não é a verdade. Em alguns casos, o mapa de visibilidade permite otimizar o processo, mas consideraremos isso com mais detalhes posteriormente.)

Ao mesmo tempo, na página de índice, encontramos indicadores para as duas versões, a atual e a antiga:

 => SELECT itemoffset, ctid FROM bt_page_items('t_s_idx',1); 
  itemoffset | ctid ------------+------- 1 | (0,2) 2 | (0,1) (2 rows) 

Transações virtuais


Na prática, o PostgreSQL usa otimizações para "salvar" os números das transações.

Se uma transação ler apenas dados, ela não afetará a visibilidade das versões de linha. Portanto, a princípio, o processo de veiculação emite uma transação de número virtual (xid virtual). O número consiste em um identificador de processo e um número seqüencial.

A emissão desse número não requer sincronização entre todos os processos e, portanto, é muito rápida. Conheceremos outro motivo para usar números virtuais quando falamos de congelamento.

Os números virtuais não são levados em consideração nos instantâneos de dados.

Em momentos diferentes, transações virtuais com números que já foram usados ​​podem aparecer no sistema, o que é normal. Mas esse número não pode ser gravado nas páginas de dados, porque na próxima vez que você acessa a página, pode perder todo o significado.

 => BEGIN; => SELECT txid_current_if_assigned(); 
  txid_current_if_assigned -------------------------- (1 row) 

Se a transação começar a alterar dados, receberá um número de transação real e exclusivo.

 => UPDATE accounts SET amount = amount - 1.00; => SELECT txid_current_if_assigned(); 
  txid_current_if_assigned -------------------------- 3667 (1 row) 

 => COMMIT; 

Transações aninhadas


Economize pontos


O SQL define pontos de salvamento que permitem desfazer uma parte de uma transação sem interrompê-la completamente. Mas isso não se enquadra no esquema acima, pois o status de uma transação é único para todas as suas alterações e, fisicamente, nenhum dado é revertido.

Para implementar essa funcionalidade, uma transação com um ponto de salvamento é dividida em várias transações aninhadas separadas (subtransação), cujo status pode ser controlado separadamente.

As transações aninhadas têm seu próprio número (maior que o número da transação principal). O status das transações aninhadas é registrado da maneira usual no XACT, no entanto, o status final depende do status da transação principal: se for cancelada, todas as transações aninhadas também serão canceladas.

As informações sobre o aninhamento de transações são armazenadas em arquivos no diretório PGDATA / pg_subtrans. Os arquivos são acessados ​​através de buffers na memória compartilhada da instância, organizados da mesma maneira que os buffers XACT.

Não confunda transações aninhadas e transações autônomas. As transações autônomas não dependem uma da outra e as aninhadas são dependentes. Não há transações autônomas no PostgreSQL usual e, talvez, para melhor: no caso de serem necessárias muito, muito raramente, e sua presença em outros DBMSs provoca abuso, do qual todos sofrem.

Limpe a tabela, inicie a transação e insira a linha:

 => TRUNCATE TABLE t; => BEGIN; => INSERT INTO t(s) VALUES ('FOO'); => SELECT txid_current(); 
  txid_current -------------- 3669 (1 row) 

 => SELECT xmin, xmax, * FROM t; 
  xmin | xmax | id | s ------+------+----+----- 3669 | 0 | 2 | FOO (1 row) 

 => SELECT * FROM heap_page('t',0); 
  ctid | state | xmin | xmax | t_ctid -------+--------+------+-------+-------- (0,1) | normal | 3669 | 0 (a) | (0,1) (1 row) 

Agora coloque um ponto de salvamento e insira outra linha.

 => SAVEPOINT sp; => INSERT INTO t(s) VALUES ('XYZ'); => SELECT txid_current(); 
  txid_current -------------- 3669 (1 row) 

Observe que a função txid_current () retorna o número da transação principal, não aninhada.

 => SELECT xmin, xmax, * FROM t; 
  xmin | xmax | id | s ------+------+----+----- 3669 | 0 | 2 | FOO 3670 | 0 | 3 | XYZ (2 rows) 

 => SELECT * FROM heap_page('t',0); 
  ctid | state | xmin | xmax | t_ctid -------+--------+------+-------+-------- (0,1) | normal | 3669 | 0 (a) | (0,1) (0,2) | normal | 3670 | 0 (a) | (0,2) (2 rows) 

Revertemos para o ponto de salvamento e inserimos a terceira linha.

 => ROLLBACK TO sp; => INSERT INTO t(s) VALUES ('BAR'); => SELECT xmin, xmax, * FROM t; 
  xmin | xmax | id | s ------+------+----+----- 3669 | 0 | 2 | FOO 3671 | 0 | 4 | BAR (2 rows) 

 => SELECT * FROM heap_page('t',0); 
  ctid | state | xmin | xmax | t_ctid -------+--------+----------+-------+-------- (0,1) | normal | 3669 | 0 (a) | (0,1) (0,2) | normal | 3670 (a) | 0 (a) | (0,2) (0,3) | normal | 3671 | 0 (a) | (0,3) (3 rows) 

Na página, continuamos a ver a linha adicionada pela transação aninhada cancelada.

Corrigimos as alterações.

 => COMMIT; => SELECT xmin, xmax, * FROM t; 
  xmin | xmax | id | s ------+------+----+----- 3669 | 0 | 2 | FOO 3671 | 0 | 4 | BAR (2 rows) 

 => SELECT * FROM heap_page('t',0); 
  ctid | state | xmin | xmax | t_ctid -------+--------+----------+-------+-------- (0,1) | normal | 3669 (c) | 0 (a) | (0,1) (0,2) | normal | 3670 (a) | 0 (a) | (0,2) (0,3) | normal | 3671 (c) | 0 (a) | (0,3) (3 rows) 

Agora você pode ver claramente que cada transação aninhada tem seu próprio status.

Observe que as transações aninhadas não podem ser usadas explicitamente no SQL, ou seja, você não pode iniciar uma nova transação sem concluir a atual. Esse mecanismo é usado implicitamente ao usar pontos de salvamento e também ao lidar com exceções do PL / pgSQL e em vários outros casos mais exóticos.

 => BEGIN; 
 BEGIN 
 => BEGIN; 
 WARNING: there is already a transaction in progress BEGIN 
 => COMMIT; 
 COMMIT 
 => COMMIT; 
 WARNING: there is no transaction in progress COMMIT 

Erros e atomicidade das operações


O que acontece se ocorrer um erro durante a operação? Por exemplo, assim:

 => BEGIN; => SELECT * FROM t; 
  id | s ----+----- 2 | FOO 4 | BAR (2 rows) 

 => UPDATE t SET s = repeat('X', 1/(id-4)); 
 ERROR: division by zero 

Ocorreu um erro. Agora a transação é considerada abortada e nenhuma operação é permitida nela:

 => SELECT * FROM t; 
 ERROR: current transaction is aborted, commands ignored until end of transaction block 

E mesmo se você tentar confirmar as alterações, o PostgreSQL reportará o cancelamento:

 => COMMIT; 
 ROLLBACK 

Por que não consigo continuar a transação após uma falha? O fato é que um erro pode ocorrer para que tenhamos acesso a parte das alterações - a atomicidade nem mesmo da transação, mas o operador seria violado. Como no nosso exemplo, onde o operador conseguiu atualizar uma linha antes do erro:

 => SELECT * FROM heap_page('t',0); 
  ctid | state | xmin | xmax | t_ctid -------+--------+----------+-------+-------- (0,1) | normal | 3669 (c) | 3672 | (0,4) (0,2) | normal | 3670 (a) | 0 (a) | (0,2) (0,3) | normal | 3671 (c) | 0 (a) | (0,3) (0,4) | normal | 3672 | 0 (a) | (0,4) (4 rows) 

Devo dizer que no psql existe um modo que ainda permite continuar a transação após uma falha, como se as ações do operador incorreto fossem revertidas.

 => \set ON_ERROR_ROLLBACK on => BEGIN; => SELECT * FROM t; 
  id | s ----+----- 2 | FOO 4 | BAR (2 rows) 

 => UPDATE t SET s = repeat('X', 1/(id-4)); 
 ERROR: division by zero 

 => SELECT * FROM t; 
  id | s ----+----- 2 | FOO 4 | BAR (2 rows) 

 => COMMIT; 

É fácil adivinhar que, nesse modo, o psql realmente define um ponto de salvaguarda implícito na frente de cada comando e, em caso de falha, inicia uma reversão para ele. Esse modo não é usado por padrão, pois a definição de pontos de salvamento (mesmo sem reverter para eles) está associada a uma sobrecarga significativa.

Para ser continuado.

Source: https://habr.com/ru/post/pt445820/


All Articles