Meça o tamanho de uma linha da tabela do PostgreSQL

83

Eu tenho uma tabela do PostgreSQL. select *é muito lento select ide agradável e rápido. Acho que o tamanho da linha é muito grande e está demorando para ser transportado, ou pode ser algum outro fator.

Eu preciso de todos os campos (ou quase todos), portanto, selecionar apenas um subconjunto não é uma solução rápida. A seleção dos campos que eu quero ainda é lenta.

Aqui está o meu esquema de tabela menos os nomes:

integer                  | not null default nextval('core_page_id_seq'::regclass)
character varying(255)   | not null
character varying(64)    | not null
text                     | default '{}'::text
character varying(255)   | 
integer                  | not null default 0
text                     | default '{}'::text
text                     | 
timestamp with time zone | 
integer                  | 
timestamp with time zone | 
integer                  | 

O tamanho do campo de texto pode ser de qualquer tamanho. Mas ainda assim, não mais do que alguns kilobytes no pior caso.

Questões

  1. Existe algo sobre isso que grita "loucamente ineficiente"?
  2. Existe uma maneira de medir o tamanho da página na linha de comando do Postgres para me ajudar a depurar isso?
Joe
fonte
Na verdade ... uma das colunas tem 11 MB. Isso explica tudo, eu acho. Então, existe uma maneira de fazer, length(*)e não apenas length(field)? Eu sei que chars não bytes, mas eu só preciso de um valor aproximado.
Joe

Respostas:

101

Q2: way to measure page size

O PostgreSQL fornece várias funções de tamanho de objeto de banco de dados . Empacotei os mais interessantes nesta consulta e adicionei algumas Funções de acesso a estatísticas na parte inferior. (O módulo adicional pgstattuple fornece funções mais úteis ainda.)

Isso mostra que métodos diferentes para medir o "tamanho de uma linha" levam a resultados muito diferentes. Tudo depende exatamente do que você deseja medir.

Esta consulta requer o Postgres 9.3 ou posterior . Para versões mais antigas, veja abaixo.

Usando uma VALUESexpressão em uma LATERALsubconsulta , para evitar a ortografia dos cálculos para cada linha.

Substitua public.tbl(duas vezes) pelo nome da tabela opcionalmente qualificada para o esquema para obter uma visualização compacta das estatísticas coletadas sobre o tamanho de suas linhas. Você pode agrupar isso em uma função plpgsql para uso repetido, inserir o nome da tabela como parâmetro e usar EXECUTE...

SELECT l.metric, l.nr AS "bytes/ct"
     , CASE WHEN is_size THEN pg_size_pretty(nr) END AS bytes_pretty
     , CASE WHEN is_size THEN nr / NULLIF(x.ct, 0) END AS bytes_per_row
FROM  (
   SELECT min(tableoid)        AS tbl      -- = 'public.tbl'::regclass::oid
        , count(*)             AS ct
        , sum(length(t::text)) AS txt_len  -- length in characters
   FROM   public.tbl t                     -- provide table name *once*
   ) x
 , LATERAL (
   VALUES
      (true , 'core_relation_size'               , pg_relation_size(tbl))
    , (true , 'visibility_map'                   , pg_relation_size(tbl, 'vm'))
    , (true , 'free_space_map'                   , pg_relation_size(tbl, 'fsm'))
    , (true , 'table_size_incl_toast'            , pg_table_size(tbl))
    , (true , 'indexes_size'                     , pg_indexes_size(tbl))
    , (true , 'total_size_incl_toast_and_indexes', pg_total_relation_size(tbl))
    , (true , 'live_rows_in_text_representation' , txt_len)
    , (false, '------------------------------'   , NULL)
    , (false, 'row_count'                        , ct)
    , (false, 'live_tuples'                      , pg_stat_get_live_tuples(tbl))
    , (false, 'dead_tuples'                      , pg_stat_get_dead_tuples(tbl))
   ) l(is_size, metric, nr);

Resultado:

              métrica | bytes / ct | bytes_pretty | bytes_per_row
----------------------------------- + ---------- + --- ----------- + ---------------
 core_relation_size | 44138496 | 42 MB | 91
 visible_map | 0 0 bytes | 0 0
 free_space_map | 32768 32 kB | 0 0
 table_size_incl_toast | 44179456 | 42 MB | 91
 indexes_size | 33128448 | 32 MB | 68
 total_size_incl_toast_and_indexes | 77307904 | 74 MB | 159
 live_rows_in_text_representation | 29987360 | 29 MB | 62
 ------------------------------ | | |
 row_count | 483424 |
 live_tuples | 483424 |
 dead_tuples | 2677 |

Para versões mais antigas (Postgres 9.2 ou anterior):

WITH x AS (
   SELECT count(*)               AS ct
        , sum(length(t::text))   AS txt_len  -- length in characters
        , 'public.tbl'::regclass AS tbl      -- provide table name as string
   FROM   public.tbl t                       -- provide table name as name
   ), y AS (
   SELECT ARRAY [pg_relation_size(tbl)
               , pg_relation_size(tbl, 'vm')
               , pg_relation_size(tbl, 'fsm')
               , pg_table_size(tbl)
               , pg_indexes_size(tbl)
               , pg_total_relation_size(tbl)
               , txt_len
             ] AS val
        , ARRAY ['core_relation_size'
               , 'visibility_map'
               , 'free_space_map'
               , 'table_size_incl_toast'
               , 'indexes_size'
               , 'total_size_incl_toast_and_indexes'
               , 'live_rows_in_text_representation'
             ] AS name
   FROM   x
   )
SELECT unnest(name)                AS metric
     , unnest(val)                 AS "bytes/ct"
     , pg_size_pretty(unnest(val)) AS bytes_pretty
     , unnest(val) / NULLIF(ct, 0) AS bytes_per_row
FROM   x, y

UNION ALL SELECT '------------------------------', NULL, NULL, NULL
UNION ALL SELECT 'row_count', ct, NULL, NULL FROM x
UNION ALL SELECT 'live_tuples', pg_stat_get_live_tuples(tbl), NULL, NULL FROM x
UNION ALL SELECT 'dead_tuples', pg_stat_get_dead_tuples(tbl), NULL, NULL FROM x;

Mesmo resultado.

Q1: anything inefficient?

Você pode otimizar a ordem das colunas para salvar alguns bytes por linha, atualmente desperdiçados no preenchimento do alinhamento:

integer                  | not null default nextval('core_page_id_seq'::regclass)
integer                  | not null default 0
character varying(255)   | not null
character varying(64)    | not null
text                     | default '{}'::text
character varying(255)   | 
text                     | default '{}'::text
text                     |
timestamp with time zone |
timestamp with time zone |
integer                  |
integer                  |

Isso economiza entre 8 e 18 bytes por linha. Eu chamo de "coluna tetris" . Detalhes:

Considere também:

Erwin Brandstetter
fonte
Seu snippet anterior à 9.3 gera uma divisão por zero se a tabela estiver vazia. Na verdade, eu queria usar a versão 9.3+, mas escolhi a errada por engano e tive que passar algumas horas consertando-a ... Agora não posso perder todo esse tempo desperdiçado. Substitua , unnest(val) / ctpor , (LEAST(unnest(val), unnest(val) * ct)) / (ct - 1 + sign(ct))e não jogará. A justificativa é que, quando ctfor 0, valserá substituída por 0e ctserá substituída por 1.
GuiRitter 17/10
1
@GuiRitter: Obrigado por apontar. Eu apliquei uma correção mais simples, no entanto. Também há algumas atualizações gerais - mas a consulta permanece a mesma.
Erwin Brandstetter
35

É fácil obter uma aproximação do tamanho de uma linha, incluindo o conteúdo do TOAST , consultando o comprimento da representação TEXT de toda a linha:

SELECT octet_length(t.*::text) FROM tablename AS t WHERE primary_key=:value;

Esta é uma aproximação aproximada ao número de bytes que serão recuperados no lado do cliente ao executar:

SELECT * FROM tablename WHERE primary_key=:value;

... supondo que o chamador da consulta solicite resultados em formato de texto, que é o que a maioria dos programas faz (o formato binário é possível, mas na maioria dos casos não vale a pena).

A mesma técnica pode ser aplicada para localizar as Nlinhas "maiores em texto" de tablename:

SELECT primary_key, octet_length(t.*::text) FROM tablename AS t
   ORDER BY 2 DESC LIMIT :N;
Daniel Vérité
fonte
Excelente maneira de obter rapidamente estimativas ao trabalhar com big data (por exemplo, a maioria do tamanho da linha está em colunas de armazenamento de brinde de comprimento variável), boa ideia!
fgblomqvist 27/03
resultado são bytes?
Akmal Salikhov
14

Existem algumas coisas que podem estar acontecendo. Em geral, duvido que o comprimento seja o problema proximal. Suspeito que você tenha um problema relacionado ao comprimento.

Você diz que os campos de texto podem chegar a alguns k. Uma linha não pode ultrapassar 8k no armazenamento principal e é provável que seus campos de texto maiores tenham sido TOASTADOS ou movidos do armazenamento principal para um armazenamento estendido em arquivos separados. Isso torna seu armazenamento principal mais rápido (portanto, o ID de seleção é realmente mais rápido porque há menos páginas em disco para acessar), mas a seleção * se torna mais lenta porque há mais E / S aleatória.

Se o tamanho total de sua linha ainda estiver bem abaixo de 8k, tente alterar as configurações de armazenamento. No entanto, eu alertaria que você pode obter coisas ruins ao inserir um atributo superdimensionado no armazenamento principal, para não tocar nisto se não for necessário e, se for o caso, defina limites apropriados por meio de restrições de verificação. Portanto, o transporte provavelmente não é a única coisa. Pode estar agrupando muitos, muitos campos que requerem leituras aleatórias. Um grande número de leituras aleatórias também pode causar falhas no cache, e uma grande quantidade de memória necessária pode exigir que as coisas se materializem no disco e um grande número de linhas largas, se houver uma junção (e houver uma se o TOAST estiver envolvido) pode exigir mais custos. juntar padrões etc.

A primeira coisa que gostaria de fazer é selecionar menos linhas e ver se isso ajuda. Se isso funcionar, você também pode tentar adicionar mais RAM ao servidor, mas eu começaria e veria onde o desempenho começa a cair devido a alterações no plano e falhas de cache primeiro.

Chris Travers
fonte
4

Usando as funções de tamanho de objeto de banco de dados mencionadas acima:

SELECT primary_key, pg_column_size(tablename.*) FROM tablename;

WhiteFire Sondergaard
fonte
parecia promissor, mas por qualquer motivo não funciona no meu caso. pg_column_size (tablename.big_column) excedeu o valor de pg_column_size (tablename. *)
linqu 17/03/16