Bem, já discutimos o
isolamento e fizemos uma digressão em relação
à estrutura de dados de baixo nível . E finalmente chegamos à coisa mais fascinante, ou seja, versões de linha (tuplas).
Cabeçalho da tupla
Como já mencionado, várias versões de cada linha podem estar disponíveis simultaneamente no banco de dados. E precisamos de alguma forma distinguir uma versão da outra. Para esse fim, cada versão é rotulada com seu “tempo” efetivo (
xmin
) e “tempo” de expiração (
xmax
). As aspas indicam que um contador de incremento especial é usado em vez do próprio tempo. E esse contador é
o identificador da transação .
(Como sempre, na realidade isso é mais complicado: o ID da transação nem sempre pode aumentar devido a uma profundidade de bits limitada do contador. Mas exploraremos mais detalhes disso quando nossa discussão chegar ao congelamento.)
Quando uma linha é criada, o valor de
xmin
é definido igual ao ID da transação que executou o comando INSERT, enquanto
xmax
não é preenchido.
Quando uma linha é excluída, o valor
xmax
da versão atual é rotulado com o ID da transação que executou DELETE.
Um comando UPDATE realmente executa duas operações subseqüentes: DELETE e INSERT. Na versão atual da linha,
xmax
é definido igual ao ID da transação que executou UPDATE. Em seguida, uma nova versão da mesma linha é criada, na qual o valor de
xmin
é o mesmo que
xmax
da versão anterior.
xmin
campos
xmin
e
xmax
são incluídos no cabeçalho de uma versão de linha. Além desses campos, o cabeçalho da tupla contém outros, como:
infomask
- vários bits que determinam as propriedades de uma determinada tupla. Existem alguns deles, e discutiremos cada um ao longo do tempo.ctid
- uma referência para a próxima versão mais recente da mesma linha. ctid
da ctid
de linha mais recente e atualizada faz referência a essa mesma versão. O número está no formato (x,y)
, onde x
é o número da página e y
é o número do pedido do ponteiro na matriz.- O bitmap NULLs, que marca as colunas de uma determinada versão que contêm um NULL. NULL não é um valor regular dos tipos de dados e, portanto, precisamos armazenar essa característica separadamente.
Como resultado, o cabeçalho parece bem grande: 23 bytes por cada tupla, no mínimo, mas geralmente maiores por causa do bitmap NULLs. Se uma tabela é "estreita" (ou seja, contém poucas colunas), os bytes gerais podem ocupar mais espaço do que as informações úteis.
Inserir
Vamos examinar mais detalhadamente como as operações nas linhas são executadas em um nível baixo e começamos com uma inserção.
Para experimentar, criaremos uma nova tabela com duas colunas e um índice em uma delas:
=> CREATE TABLE t( id serial, s text ); => CREATE INDEX ON t(s);
Iniciamos uma transação para inserir uma linha.
=> BEGIN; => INSERT INTO t(s) VALUES ('FOO');
Este é o ID da nossa transação atual:
=> SELECT txid_current();
txid_current -------------- 3664 (1 row)
Vamos examinar o conteúdo da página. A função heap_page_items da extensão "pageinspect" permite obter informações sobre os 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" no PostgreSQL indica tabelas. Este é mais um uso estranho de um termo: um heap é uma
estrutura de dados conhecida, que não tem nada a ver com uma tabela. Essa palavra é usada aqui no sentido de que "tudo está amontoado", diferente dos índices ordenados.
Esta função mostra os dados "como estão", em um formato difícil de compreender. Para esclarecer as coisas, deixamos apenas parte das informações e as interpretamos:
=> 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)
Fizemos o seguinte:
- Adicionado zero ao número do ponteiro para torná-lo parecido com um
t_ctid
: (número da página, número do ponteiro). - Interpretou o status do ponteiro
lp_flags
. Aqui é "normal", o que significa que o ponteiro realmente faz referência a uma versão de linha. Discutiremos outros valores posteriormente. - De todos os bits de informação, selecionamos apenas dois pares até agora.
xmin_committed
bits xmin_committed
e xmin_aborted
mostram se a transação com o ID xmin
está confirmada (revertida). Um par de bits semelhantes está relacionado à transação com o ID xmax
.
O que observamos? Quando uma linha é inserida, na página da tabela, um ponteiro aparece com o número 1 e faz referência à primeira e à única versão da linha.
O campo
xmin
na tupla é preenchido com o ID da transação atual. Como a transação ainda está ativa, os bits
xmin_aborted
e
xmin_aborted
não estão configurados.
O campo
ctid
da versão da linha faz referência à mesma linha. Isso significa que nenhuma versão mais recente está disponível.
O campo
xmax
é preenchido com o número convencional 0, pois a tupla não é excluída, ou seja, atualizada. As transações ignoram esse número devido ao conjunto de bits
xmax_aborted
.
Vamos dar mais um passo para melhorar a legibilidade acrescentando bits de informação aos IDs de transação. E vamos criar a função, pois precisaremos da consulta 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;
O que está acontecendo no cabeçalho da versão da linha é muito mais claro neste formato:
=> SELECT * FROM heap_page('t',0);
ctid | state | xmin | xmax | t_ctid -------+--------+------+-------+-------- (0,1) | normal | 3664 | 0 (a) | (0,1) (1 row)
Podemos obter informações semelhantes, mas muito menos detalhadas, 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)
Confirmar
Quando uma transação é bem-sucedida, seu status deve ser lembrado, ou seja, a transação deve ser marcada como confirmada. Para esse fim, a estrutura XACT é usada. (Antes da versão 10, era chamado CLOG (commit log), e é provável que você encontre esse nome.)
XACT não é uma tabela do catálogo do sistema, mas arquivos no diretório PGDATA / pg_xact. Dois bits são alocados nesses arquivos para cada transação - "confirmada" e "abortada" - exatamente da mesma maneira que no cabeçalho da tupla. Essas informações estão espalhadas por vários arquivos apenas por conveniência; voltaremos a isso quando discutirmos o congelamento. O PostgreSQL trabalha com esses arquivos página por página, como em todos os outros.
Portanto, quando uma transação é confirmada, o bit "confirmado" é definido para essa transação no XACT. E isso é tudo o que acontece quando a transação é confirmada (embora ainda não mencionemos o log de gravação antecipada).
Quando alguma outra transação acessar a página da tabela que estávamos vendo, a primeira terá que responder a algumas perguntas.
- A transação
xmin
concluída? Caso contrário, a tupla criada não deve estar visível.
Isso é verificado olhando através de outra estrutura, localizada na memória compartilhada da instância e chamada ProcArray. Essa estrutura mantém uma lista de todos os processos ativos, juntamente com o ID da transação atual (ativa) de cada um. - Se a transação foi concluída, ela foi confirmada ou revertida? Se foi revertida, a tupla também não deve estar visível.
É exatamente para isso que o XACT é necessário. Mas é caro verificar o XACT a cada vez, embora as últimas páginas do XACT sejam armazenadas em buffers na memória compartilhada. Portanto, uma vez descoberto, o status da transação é gravado nos bits xmin_aborted
e xmin_aborted
da tupla. Se qualquer um desses bits estiver definido, o status da transação será tratado como conhecido e a próxima transação não precisará verificar XACT.
Por que a transação que executa a inserção define esses bits? Quando uma inserção está sendo executada, a transação ainda não sabe se será concluída com êxito. E no momento da confirmação já não está claro quais linhas e quais páginas foram alteradas. Pode haver muitas dessas páginas e é impraticável acompanhá-las. Além disso, algumas das páginas podem ser despejadas em disco do cache do buffer; lê-los novamente para alterar os bits significaria uma desaceleração significativa do commit.
O lado oposto da economia de custos é que, após as atualizações, qualquer transação (mesmo a que executa o SELECT) pode começar a alterar as páginas de dados no cache do buffer.
Então, comprometemos a mudança.
=> COMMIT;
Nada mudou na página (mas sabemos que o status das transações já está gravado 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, uma transação que acessa primeiro a página precisará 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 ID da transação de exclusão atual é gravado no campo
xmax
da versão atualizada e o bit
xmax_aborted
é redefinido.
Observe que o valor de
xmax
correspondente à transação ativa funciona como um bloqueio de linha. Se outra transação for atualizar ou excluir esta linha, ela terá que esperar até que a transação
xmax
concluída. Falaremos sobre bloqueios com mais detalhes posteriormente. Nesse ponto, observe apenas que o número de bloqueios de linha não é limitado. Eles não ocupam memória e o desempenho do sistema não é afetado por esse número. No entanto, transações de longa duração têm outras desvantagens, que também serão discutidas mais adiante.
Vamos excluir uma linha.
=> BEGIN; => DELETE FROM t; => SELECT txid_current();
txid_current -------------- 3665 (1 row)
Vemos que o ID da transação é gravado 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)
Abortar
O cancelamento de uma transação funciona da mesma forma que o commit, exceto que o bit "cancelado" está definido no XACT. Um cancelamento é feito tão rápido quanto um commit. Embora o comando seja chamado ROLLBACK, as alterações não são revertidas: tudo o que a transação já mudou, permanece intocado.
=> 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. Embora o próprio número
xmax
ainda esteja na página, ele não será visto.
=> 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
Uma atualização funciona como se a versão atual fosse excluída primeiro e depois uma nova fosse inserida.
=> BEGIN; => UPDATE t SET s = 'BAR'; => SELECT txid_current();
txid_current -------------- 3666 (1 row)
A consulta retorna uma linha (a nova versão):
=> SELECT * FROM t;
id | s ----+----- 1 | BAR (1 row)
Mas podemos ver as duas versões na página:
=> 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 excluída é rotulada com o ID da transação atual no campo
xmax
. Além disso, esse valor substituiu o antigo desde que a transação anterior foi revertida. E o bit
xmax_aborted
é redefinido, pois o status da transação atual ainda é desconhecido.
A primeira versão da linha agora está fazendo referência à segunda, como uma mais nova.
A página de índice agora contém o segundo ponteiro e a segunda linha, que referencia a segunda versão na página da tabela.
Da mesma maneira que para uma exclusão, o valor de
xmax
na primeira versão indica que a linha está bloqueada.
Por fim, confirmamos a transação.
=> COMMIT;
Índices
Estávamos conversando apenas sobre as páginas da tabela até agora. Mas o que acontece dentro dos índices?
As informações nas páginas de índice dependem muito do tipo de índice específico. Além disso, mesmo um tipo de índices pode ter diferentes tipos de páginas. Por exemplo: uma árvore B possui a página de metadados e as páginas "normais".
No entanto, uma página de índice geralmente possui uma matriz de ponteiros para as linhas e linhas (como as páginas da tabela). Além disso, algum espaço no final de uma página é alocado para dados especiais.
Linhas em índices também podem ter estruturas diferentes, dependendo do tipo de índice. Por exemplo: em uma árvore B, as linhas pertinentes às páginas folha contêm o valor da chave de indexação e uma referência (
ctid
) à linha da tabela apropriada. Em geral, um índice pode ser estruturado de uma maneira bem diferente.
O ponto principal é que, em índices de qualquer tipo, não há
versões de linha. Ou podemos considerar que cada linha seja representada por apenas uma versão. Em outras palavras, o cabeçalho da linha de índice não contém os campos
xmin
e
xmax
. Por enquanto, podemos assumir que as referências do índice apontam para todas as versões das linhas da tabela. Portanto, para descobrir quais versões de linha são visíveis para uma transação, o PostgreSQL precisa examinar a tabela. (Como sempre, essa não é a história toda. Às vezes, o mapa de visibilidade permite otimizar o processo, mas discutiremos isso mais tarde.)
Aqui, na página de índice, encontramos indicadores para ambas as versões: a atualizada e a anterior:
=> 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 tira proveito de uma otimização que permite gastar "moderadamente" os IDs de transação.
Se uma transação apenas lê dados, ela não afeta a visibilidade da tupla. Portanto, primeiro o processo de back-end atribui um ID virtual (xid virtual) à transação. Esse ID consiste no identificador do processo e em um número seqüencial.
A atribuição desse ID virtual não requer sincronização entre todos os processos e, portanto, é realizada muito rapidamente. Aprenderemos outro motivo do uso de IDs virtuais quando discutirmos o congelamento.
Os instantâneos de dados não levam em consideração o ID virtual.
Em diferentes momentos, o sistema pode ter transações virtuais com IDs que já foram usados, e isso é bom. Mas esse ID não pode ser gravado nas páginas de dados, pois quando a página é acessada na próxima vez, o ID pode se tornar sem sentido.
=> BEGIN; => SELECT txid_current_if_assigned();
txid_current_if_assigned -------------------------- (1 row)
Porém, se uma transação começar a alterar dados, ela receberá um ID de transação verdadeiro e exclusivo.
=> UPDATE accounts SET amount = amount - 1.00; => SELECT txid_current_if_assigned();
txid_current_if_assigned -------------------------- 3667 (1 row)
=> COMMIT;
Subtransações
Savepoints
No SQL, são definidos pontos de
salvamento , que permitem reverter algumas operações da transação sem o aborto completo. Mas isso é incompatível com o modelo acima, pois o status da transação é um para todas as alterações e nenhum dado é fisicamente revertido.
Para implementar essa funcionalidade, uma transação com um ponto de salvamento é dividida em várias
subtransações separadas cujos status podem ser gerenciados separadamente.
As subtrabsações têm seus próprios IDs (maiores que o ID da transação principal). Os status das subtransações são gravados no XACT da maneira usual, mas o status final depende do status da transação principal: se for revertida, todas as subtransações também serão revertidas.
As informações sobre o aninhamento de subtransações são armazenadas nos arquivos do diretório PGDATA / pg_subtrans. Esses arquivos são acessados por meio de buffers na memória compartilhada da instância, estruturados da mesma maneira que os buffers XACT.
Não confunda subtransações com transações autônomas. As transações autônomas não dependem uma da outra, enquanto as subtransações dependem. Não há transações autônomas no PostgreSQL comum, o que é, talvez, para melhor: elas são realmente necessárias muito raramente, e sua disponibilidade em outros DBMS convida a abusos, que todos sofrem.
Vamos limpar a tabela, iniciar uma transação e inserir uma 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, estabelecemos um ponto de salvamento e inserimos 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 ID da transação principal em vez da subtransação.
=> 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)
Vamos reverter para o ponto de salvamento e inserir a terceira linha.
=> ROLLBACK TO sp; => INSERT INTO t 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 que foi adicionada pela subtransação revertida.
Confirmando 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)
É claramente visto agora que cada subtransação tem seu próprio status.
Observe que o SQL não permite o uso explícito de subtransações, ou seja, você não pode iniciar uma nova transação antes de concluir a atual. Essa técnica se envolve implicitamente quando pontos de salvamento são usados e também ao lidar com exceções do PL / pgSQL, bem como em outras situações mais exóticas.
=> 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 da operação
O que acontece se ocorrer um erro enquanto a operação estiver sendo executada? 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 é tratada como 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 tentarmos confirmar as alterações, o PostgreSQL reportará a reversão:
=> COMMIT;
ROLLBACK
Por que é impossível continuar a execução da transação após uma falha? O problema é que o erro poderia ocorrer para que tivéssemos acesso a parte das alterações, ou seja, a atomicidade seria interrompida não apenas para a transação, mas também para um único operador. Por exemplo, em nosso exemplo, o operador poderia ter atualizado 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)
Vale ressaltar que o psql possui um modo que permite continuar a transação após a falha, como se os efeitos do operador incorreto fossem revertidos.
=> \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 descobrir que, nesse modo, o psql realmente estabelece um ponto de salvaguarda implícito antes de cada comando e inicia uma reversão para ele em caso de falha. Esse modo não é usado por padrão, pois o estabelecimento de pontos de salvamento (mesmo sem reversão) implica uma sobrecarga significativa.
Continue lendo .