50.3. Disposição das páginas de banco de dados

Esta seção fornece uma visão geral do formato de página utilizado dentro das tabelas e índices do PostgreSQL. [1] As tabelas de seqüência e TOAST são formatadas como qualquer outra tabela regular.

Na explicação a seguir, é assumido que um byte tem 8 bits. Além disso, o termo item se refere a um valor de dado individual armazenado na página. Em uma tabela, um item é uma linha; em um índice, um item é uma entrada do índice.

Todas as tabelas e índices são armazenadas em uma matriz de páginas de tamanho fixo (geralmente 8Kb, embora possa ser selecionado um tamanho de página diferente ao compilar o servidor). Em uma tabela todas as páginas são logicamente equivalentes, portanto um determinado item (linha) pode ser armazenado em qualquer página. Nos índices, a primeira página geralmente é reservada para uma metapágina contendo informações de controle, e podem existir tipos diferentes de página dentro do índice, dependendo do método de acesso do índice.

A Tabela 50-2 mostra a disposição global da página. Existem cinco partes em cada página.

Tabela 50-2. Disposição global da página

Item Descrição
PageHeaderData Comprimento de 20 bytes. Contém informações gerais sobre a página, incluindo ponteiros para espaços livres.
ItemPointerData Matriz de pares (deslocamento, comprimento) apontando para os itens existentes. 4 bytes por item.
Espaço livre Espaço não alocado. Os novos ponteiros de item são alocados a partir do início desta área, e os novos itens a partir do fim.
Itens Os próprios itens existentes.
Espaço especial Dados específicos do método de acesso do índice. Métodos diferentes armazenam dados diferentes. Vazio nas tabelas comuns.

Os primeiros 20 bytes de cada página compõem o cabeçalho da página (PageHeaderData). Seu formato é detalhado na Tabela 50-3. Os primeiros dois campos registram a entrada mais recente no WAL relacionada a esta página. São seguidos por três campos inteiros de 2 bytes (pd_lower, pd_upper, e pd_special). Estes campos contêm o deslocamento em bytes do início da página ao início do espaço não alocado, ao final do espaço não alocado, e ao início do espaço especial. Os últimos 2 bytes do cabeçalho da página, pd_pagesize_version, armazenam o tamanho da página e o indicador de versão. A partir do PostgreSQL 8.0 o número da versão é 2; O PostgreSQL 7.3 e 7.4 usam a versão número 1; as versões anteriores usam a versão número 0 (A disposição básica da página e o formato do cabeçalho não mudaram nestas versões, mas a disposição dos cabeçalhos das linhas heap mudou). Basicamente, o tamanho da página somente está presente como uma verificação cruzada; não há suporte para a existência de mais de um tamanho de página em uma instalação.

Tabela 50-3. Disposição de PageHeaderData

Campo Tipo Comprimento Descrição
pd_lsn XLogRecPtr 8 bytes LSN: próximo byte após o último byte do registro do xlog (gerenciador do registro de transação do PostgreSQL) para a última modificação nesta página
pd_tli TimeLineID 4 bytes TLI da última mudança
pd_lower LocationIndex 2 bytes Deslocamento até o início do espaço livre
pd_upper LocationIndex 2 bytes Deslocamento até o final do espaço livre
pd_special LocationIndex 2 bytes Deslocamento até o início do espaço especial
pd_pagesize_version uint16 2 bytes Informação sobre o tamanho em bytes e número da versão de disposição da página

Todos os detalhes podem ser encontrados no arquivo src/include/storage/bufpage.h. [2]

/*
 * A página de disco do postgres é uma camada de abstração por cima do
 * bloco de disco do postgres (que é simplesmente uma unidade de E/S)
 * (veja block.h).
 *
 * Especificamente, enquanto um bloco de disco pode estar não-formatado,
 * uma página de disco é sempre uma página com encaixes na forma:
 *
 * +----------------+-----------------------------------+
 * | PageHeaderData | linp1 linp2 linp3 ...             |
 * +-----------+----+-----------------------------------+
 * | ... linpN |                                        |
 * +-----------+----------------------------------------+
 * |           ^ pd_lower                               |
 * |                                                    |
 * |             v pd_upper                             |
 * +-------------+--------------------------------------+
 * |             | tuplaN ...                           |
 * +-------------+------------------+-------------------+
 * |       ... tupla3 tupla2 tupla1 | "espaço especial" |
 * +--------------------------------+-------------------+
 *                                  ^ pd_special
 *
 * a página fica cheia quando não pode ser adicionado mais nada entre
 * pd_lower e pd_upper.
 *
 * todos os blocos escritos por um método de acesso devem ser páginas de disco.
 *
 * EXCEÇÕES:
 *
 * como é óbvio, a página não é formatada antes de ser inicializada por uma
 * chamada a PageInit.
 *
 * NOTAS:
 *
 * linp1..N formam uma matriz de ItemId. Os ItemPointers apontam para esta
 * matriz em vez de apontar diretamente para a tupla. Deve ser observado que
 * OffsetNumbers convencionalmente começa por 1, e não por 0.
 *
 * tupla1..N são adicionadas na página "de trás para frente".
 * Como o ItemPointer da tupla aponta para a sua entrada ItemId, em vez de
 * apontar para a sua posição real medida em deslocamento em bytes, as tuplas
 * podem ser fisicamente embaralhadas na página sempre que houver necessidade.
 *
 * Informações genéricas por página do método de acesso são mantidas em
 * PageHeaderData.
 *
 * Informações específicas por página do método de acesso (se existirem) são
 * mantidas na área marcada como "espaço especial"; cada método de acesso
 * possui uma estrutura "opaca" definida em algum lugar que é armazenada como
 * o rodapé da página. O método de acesso deve inicializar sempre suas páginas
 * com PageInit, e depois definir seus próprios campos opacos.
 */

typedef Pointer Page;


/*
 * posição (deslocamento em bytes) dentro da página.
 *
 * deve ser observado que na verdade está limitado a 2^15, porque
 * ItemIdData.lp_off e ItemIdData.lp_len foram limitados a 15 bits
 * (veja itemid.h).
 */
typedef uint16 LocationIndex;


/*
 * organização da página do disco
 *
 * informações de gerenciamento de espaço genéricas para qualquer página
 *
 *    pd_lsn
 *       identifica o registro do xlog para a última modificação nesta página.
 *    pd_tli
 *       a mesma coisa.
 *    pd_lower
 *       deslocamento até o início do espaço livre
 *    pd_upper
 *       deslocamento até o final do espaço livre
 *    pd_special
 *       deslocamento até o início do espaço especial
 *    pd_pagesize_version
 *        tamanho em bytes e número da versão de disposição da página
 *
 * O LSN é utilizado pelo gerenciador de buffers para garantir a regra
 * básica do WAL: "deve ser escrito em xlog antes de escrever os dados".
 * Um buffer sujo não pode ser descarregado no disco até que xlog tenha
 * sido descarregado atingindo pelo menos o LSN da página.
 * O TLI também é armazenado para fins de identificação (não é claro se é
 * realmente necessário, mas parece ser uma boa idéia).
 *
 * O número da versão da página e o tamanho da página são empacotados juntos
 * em um único campo uint16. Isto se deve a motivos históricos: antes do
 * PostgreSQL 7.3, não havia o conceito de número de versão de página,
 * e fazendo desta maneira fingimos que os bancos de dados anteriores a
 * versão 7.3 possuem o número de versão de página zero.
 * Os tamanhos de página são restritos a múltiplos de 256, deixando os 8 bits
 * de mais baixa ordem disponíveis para o número da versão.
 *
 * O tamanho mínimo possível de uma página é, talvez, 64B para caber o
 * cabeçalho da página, o espaço opaco e uma tupla mínima; obviamente,
 * na realidade se deseja um tamanho muito maior e, portanto, a restrição
 * do tamanho da página ser múltiplo de 256 não é uma restrição importante.
 * Do lado mais alto, só é possível suportar páginas de até 32KB, porque
 * lp_off/lp_len são 15 bits.
 */
typedef struct PageHeaderData
{
    /* XXX LSN é membro de *qualquer* bloco, não apenas os organizados por página */
    XLogRecPtr       pd_lsn;         /* LSN: próximo byte após o último byte
                                      * do registro do xlog para a última
                                      * modificação nesta página */
    TimeLineID       pd_tli;         /* TLI da última modificação */
    LocationIndex    pd_lower;       /* Deslocamento até o início de espaço livre */
    LocationIndex    pd_upper;       /* Deslocamento até o final de espaço livre */
    LocationIndex    pd_special;     /* Deslocamento até o início de espaço especial */
    uint16           pd_pagesize_version;
    ItemIdData       pd_linp[1];     /* Início da matriz de ponteiro de linha */
} PageHeaderData;

typedef PageHeaderData *PageHeader;

/*
 * O número de versão de disposição de página 0 é usado nas versões
 * anteriores a 7.3 do Postgres. As versões 7.3 e 7.4 utilizam 1,
 * denotando a nova disposição de HeapTupleHeader. A versão 8.0 mudou
 * a disposição de HeapTupleHeader novamente.
 */
#define PG_PAGE_LAYOUT_VERSION      2

Depois do cabeçalho da página estão os identificadores de itens (ItemIdData), cada um requerendo quatro bytes. O identificador de item contém o deslocamento em bytes até o início do item, o comprimento do item em bytes, e uns poucos bits de atributo que afetam a interpretação do item. Os novos identificadores são alocados, conforme a necessidade, a partir do início do espaço não alocado. O número de itens identificadores presentes pode ser determinado olhando pd_lower, que é aumentado para abrir espaço para o novo identificador. Como um identificador de item nunca é movido até que seja liberado, seu índice pode ser utilizado por longo prazo para referenciar um item, mesmo quando o próprio item é movido dentro da página para compactar o espaço livre. Na verdade, todo ponteiro para um item (ItemPointer, também conhecido como CTID) criado pelo PostgreSQL, consiste de um número de página e o índice do identificador do item.

Os itens em si são armazenados em um espaço obtido de trás para frente a partir do final do espaço não alocado. A estrutura exata varia conforme o conteúdo da tabela. As tabelas e as seqüências utilizam uma estrutura chamada HeapTupleHeaderData, descrita abaixo.

A seção final é a "seção especial", e pode conter qualquer coisa que o método de acesso deseje armazenar. Por exemplo, os índices B-tree armazenam vínculos para os irmãos (siblings) esquerdo e direito da página, assim como alguns outros dados relevantes para a estrutura do índice. As tabelas comuns não utilizam a seção especial (indicado pela definição pd_special igual ao tamanho da página).

Todas as linhas da tabela são estruturadas da mesma maneira. Existe um cabeçalho de tamanho fixo (ocupando 27 bytes na maioria das máquinas), seguido por um mapa de bits de nulo opcional, um campo do ID do objeto opcional, e os dados do usuário. O cabeçalho está detalhado na Tabela 50-4. Os dados verdadeiros (colunas da linha) começam no primeiro deslocamento indicado por t_hoff, que sempre deve ser um múltiplo da distância MAXALIGN para a plataforma. O mapa de bits de nulo somente está presente se o bit HEAP_HASNULL estiver definido em t_infomask. Se estiver presente, começa logo após o cabeçalho fixo e ocupa uma quantidade de bytes suficiente para ter um bit para cada coluna de dados (ou seja, t_natts bits no total). Nesta lista de bits, o bit 1 indica não-nulo, e o bit 0 indica nulo. Quando o mapa de bits não está presente, é assumido que o valor de todas as colunas é diferente de nulo (não-nulas). O ID do objeto só está presente quando o bit HEAP_HASOID está definido em t_infomask. Se estiver presente, aparece logo antes da fronteira de t_hoff. Qualquer enchimento necessário para tornar t_hoff um múltiplo de MAXALIGN fica entre o mapa de bits de nulo e o ID do objeto (Por sua vez, isto garante que o ID do objeto está alinhado de forma apropriada).

Tabela 50-4. Disposição de HeapTupleHeaderData

Campo Tipo Comprimento Descrição
t_xmin TransactionId 4 bytes marca do XID de inserção
t_cmin CommandId 4 bytes marca do CID de inserção
t_xmax TransactionId 4 bytes marca do XID de exclusão
t_cmax CommandId 4 bytes marca do CID de exclusão (sobrepõe t_xvac)
t_xvac TransactionId 4 bytes XID da operação de VACUUM movendo a versão da linha
t_ctid ItemPointerData 6 bytes TID corrente desta ou de uma nova versão da linha
t_natts int16 2 bytes número de atributos
t_infomask uint16 2 bytes vários bits sinalizadores
t_hoff uint8 1 byte deslocamento até os dados do usuário

Todos os detalhes se encontram em src/include/access/htup.h.

A interpretação dos dados reais somente pode ser feita com informações obtidas a partir de outras tabelas, principalmente pg_attribute. Os valores chave necessários para identificar a posição do campo são attlen e attalign. Não há maneira de obter um determinado atributo diretamente, exceto quando existem somente campos de largura fixa e nenhum nulo. Todos os truques estão contidos nas funções heap_getattr, fastgetattr e heap_getsysattr.

Para ler os dados é necessário examinar cada atributo por vez. Primeiro deve ser verificado se o campo é nulo utilizando o mapa de bits de nulo. Se for, deve-se ir para o próximo. Depois deve haver certeza de estar no alinhamento correto. Se o campo for de largura fixa, então todos os bytes estão simplesmente colocados em seus lugares. Se for um campo de largura variável (attlen = -1), então é um pouco mais complicado. Todos os tipos de dado de comprimento variável compartilham uma estrutura de cabeçalho comum, varattrib, que inclui o comprimento total do valor armazenado e alguns bits sinalizadores. Dependendo dos sinalizadores, os dados podem estar em-linha ou em uma tabela TOAST; também pode estar comprimido (consulte a Seção 50.2).

Notas

[1]

Na verdade, os métodos de acesso de índice não precisam utilizar este formato de página. Todos os métodos de índice existentes utilizam este formato básico, mas os dados mantidos nas metapáginas dos índices geralmente não seguem as regras de disposição de item.

[2]

O trecho do arquivo bufpage.h mostrado abaixo não faz parte do manual original. (N. do T.)

SourceForge.net Logo CSS válido!