Mida el tamaño de una fila de tabla PostgreSQL

83

Tengo una tabla PostgreSQL. select *es muy lento, mientras que select ides agradable y rápido. Creo que puede ser que el tamaño de la fila sea muy grande y que lleve un tiempo transportarlo, o puede ser otro factor.

Necesito todos los campos (o casi todos), por lo que seleccionar solo un subconjunto no es una solución rápida. Seleccionar los campos que quiero todavía es lento.

Aquí está mi esquema de tabla menos los nombres:

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                  | 

El tamaño del campo de texto puede ser de cualquier tamaño. Pero aún así, no más de unos pocos kilobytes en el peor de los casos.

Preguntas

  1. ¿Hay algo en esto que grite "loco ineficiente"?
  2. ¿Hay alguna manera de medir el tamaño de página en la línea de comandos de Postgres para ayudarme a depurar esto?
Joe
fuente
En realidad ... una de las columnas es de 11 MB. Eso lo explicará, creo. Entonces, ¿hay alguna manera de hacerlo en length(*)lugar de solo length(field)? Sé que son caracteres, no bytes, pero solo necesito un valor aproximado.
Joe

Respuestas:

101

Q2: way to measure page size

PostgreSQL proporciona una serie de funciones de tamaño de objeto de base de datos . Empaqué los más interesantes en esta consulta y agregué algunas funciones de acceso a estadísticas en la parte inferior. (El módulo adicional pgstattuple proporciona aún más funciones útiles).

Esto mostrará que los diferentes métodos para medir el "tamaño de una fila" conducen a resultados muy diferentes. Todo depende de lo que quieras medir, exactamente.

Esta consulta requiere Postgres 9.3 o posterior . Para versiones anteriores ver más abajo.

Usar una VALUESexpresión en una LATERALsubconsulta para evitar deletrear cálculos para cada fila

Reemplace public.tbl(dos veces) con el nombre de la tabla opcionalmente calificado por el esquema para obtener una vista compacta de las estadísticas recopiladas sobre el tamaño de sus filas. Puede envolver esto en una función plpgsql para uso repetido, entregar el nombre de la tabla como parámetro y 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_por_row
----------------------------------- + ---------- + --- ----------- + ---------------
 core_relation_size | 44138496 | 42 MB | 91 91
 visibilidad_mapa | 0 | 0 bytes | 0 0
 free_space_map | 32768 | 32 kB | 0 0
 table_size_incl_toast | 44179456 | 42 MB | 91 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 62
 ------------------------------ | El | El |
 row_count | 483424 | El |
 live_tuples | 483424 | El |
 dead_tuples | 2677 | El |

Para versiones anteriores (Postgres 9.2 o 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;

Mismo resultado.

Q1: anything inefficient?

Puede optimizar el orden de las columnas para guardar algunos bytes por fila, actualmente desperdiciados en el relleno de alineación:

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                  |

Esto ahorra entre 8 y 18 bytes por fila. Lo llamo "columna tetris" . Detalles:

Considere también:

Erwin Brandstetter
fuente
Su fragmento pre 9.3 arroja una división por cero si la tabla está vacía. En realidad, quería usar la versión 9.3+, pero elegí la incorrecta por error y tuve que pasar algunas horas arreglándola ... Ahora no puedo dejar que se pierda todo ese tiempo. Reemplace , unnest(val) / ctpor , (LEAST(unnest(val), unnest(val) * ct)) / (ct - 1 + sign(ct))y no se lanzará. La justificación es que, cuando ctsea 0, valserá reemplazado por 0y ctserá reemplazado por 1.
GuiRitter
1
@GuiRitter: Gracias por señalar. Sin embargo, apliqué una solución más simple. También algunas actualizaciones generales mientras se realiza, pero la consulta sigue siendo la misma.
Erwin Brandstetter
35

Es fácil obtener una aproximación del tamaño de una fila, incluidos los contenidos editados de TOAST , al consultar la longitud de la representación de TEXTO de toda la fila:

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

Esta es una aproximación cercana al número de bytes que se recuperarán del lado del cliente al ejecutar:

SELECT * FROM tablename WHERE primary_key=:value;

... suponiendo que la persona que llama de la consulta solicita resultados en formato de texto, que es lo que hacen la mayoría de los programas (el formato binario es posible, pero no vale la pena en la mayoría de los casos).

Se podría aplicar la misma técnica para ubicar las Nfilas "más grandes en el texto" de tablename:

SELECT primary_key, octet_length(t.*::text) FROM tablename AS t
   ORDER BY 2 DESC LIMIT :N;
Daniel Vérité
fuente
Excelente manera de obtener rápidamente algunas estimaciones cuando se trabaja con grandes datos (por ejemplo, la mayoría del tamaño de la fila se encuentra en columnas almacenadas en tostadas de longitud variable), ¡buena idea!
fgblomqvist
resultado son bytes?
Akmal Salikhov
14

Hay algunas cosas que podrían estar sucediendo. En general, dudo que la longitud sea el problema proximal. Sospecho que en cambio tienes un problema relacionado con la longitud.

Dices que los campos de texto pueden llegar a unos pocos k. Una fila no puede superar los 8k en el almacenamiento principal, y es probable que sus campos de texto más grandes se TOSTEN o se hayan movido del almacenamiento principal a un almacenamiento extendido en archivos separados. Esto hace que su almacenamiento principal sea más rápido (por lo que seleccionar id en realidad es más rápido porque tiene menos páginas de disco para acceder) pero select * se vuelve más lento porque hay más E / S aleatorias.

Si el tamaño total de las filas sigue siendo muy inferior a 8k, podría intentar alterar la configuración de almacenamiento. Sin embargo, advierto que puede hacer que sucedan cosas malas al insertar un atributo de gran tamaño en el almacenamiento principal, por lo que es mejor no tocar esto si no es necesario y, si lo hace, establezca los límites apropiados a través de las restricciones de verificación. Por lo tanto, el transporte no es lo único probable. Puede estar recopilando muchos, muchos campos que requieren lecturas aleatorias. Un gran número de lecturas aleatorias también puede causar errores de caché, y una gran cantidad de memoria requerida puede requerir que las cosas se materialicen en el disco y un gran número de filas anchas, si hay una unión (y hay una si TOAST está involucrado) puede requerir costosos patrones de unión, etc.

Lo primero que consideraría hacer es seleccionar menos filas y ver si eso ayuda. Si eso funciona, podría intentar agregar más RAM al servidor también, pero comenzaría y vería dónde comienza a disminuir el rendimiento debido a los cambios en el plan y las fallas de caché primero.

Chris Travers
fuente
4

Uso de las funciones de tamaño de objeto de base de datos mencionadas anteriormente:

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

WhiteFire Sondergaard
fuente
Parecía prometedor, pero por alguna razón no funciona en mi caso. pg_column_size (tablename.big_column) excedió el valor de pg_column_size (tablename. *)
linqu