Messen Sie die Größe einer PostgreSQL-Tabellenzeile

83

Ich habe eine PostgreSQL-Tabelle. select *ist sehr langsam während select idist schön und schnell. Ich denke, es kann sein, dass die Größe der Reihe sehr groß ist und der Transport eine Weile dauert, oder es kann ein anderer Faktor sein.

Ich benötige alle Felder (oder fast alle), daher ist die Auswahl einer Teilmenge keine schnelle Lösung. Das Auswählen der gewünschten Felder ist immer noch langsam.

Hier ist mein Tabellenschema ohne die Namen:

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                  | 

Die Größe des Textfeldes kann beliebig sein. Trotzdem nicht mehr als ein paar Kilobyte im schlimmsten Fall.

Fragen

  1. Gibt es irgendetwas, das "verrückt ineffizient" schreit?
  2. Gibt es eine Möglichkeit, die Seitengröße in der Postgres-Befehlszeile zu messen, um das Debuggen zu erleichtern?
Joe
quelle
Eigentlich ist eine der Spalten 11 MB groß. Das wird es erklären, denke ich. Gibt es also eine Möglichkeit, etwas zu tun, length(*)anstatt es nur zu tun length(field)? Ich weiß, dass Zeichen keine Bytes sind, aber ich brauche nur einen ungefähren Wert.
Joe

Antworten:

101

Q2: way to measure page size

PostgreSQL bietet eine Reihe von Funktionen für die Größe von Datenbankobjekten . Ich habe die interessantesten in dieser Abfrage gepackt und unten einige Statistik-Zugriffsfunktionen hinzugefügt . (Das Zusatzmodul pgstattuple bietet noch weitere nützliche Funktionen.)

Dies wird zeigen, dass verschiedene Methoden zur Messung der "Größe einer Zeile" zu sehr unterschiedlichen Ergebnissen führen. Alles hängt davon ab, was Sie genau messen möchten.

Diese Abfrage erfordert Postgres 9.3 oder höher . Für ältere Versionen siehe unten.

Verwenden eines VALUESAusdrucks in einer LATERALUnterabfrage , um zu vermeiden, dass Berechnungen für jede Zeile geschrieben werden.

Ersetzen Sie public.tbl(zweimal) durch Ihren optionalen schemaqualifizierten Tabellennamen, um eine kompakte Ansicht der gesammelten Statistiken über die Größe Ihrer Zeilen zu erhalten. Sie können dies zur wiederholten Verwendung in eine plpgsql-Funktion einbinden, den Tabellennamen als Parameter übergeben und 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);

Ergebnis:

              metrisch | Bytes / ct | bytes_pretty | bytes_per_row
----------------------------------- + ---------- + --- ----------- + ---------------
 core_relation_size | 44138496 | 42 MB | 91
 sichtbarkeitskarte | 0 | 0 Bytes | 0
 free_space_map | 32768 | 32 kB | 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 | |

Für ältere Versionen (Postgres 9.2 oder älter):

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;

Gleiches Ergebnis.

Q1: anything inefficient?

Sie können die Spaltenreihenfolge optimieren , um einige Bytes pro Zeile zu sparen, die derzeit für das Ausrichtungs-Padding verschwendet werden:

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                  |

Dies spart zwischen 8 und 18 Bytes pro Zeile. Ich nenne es "Spalte Tetris" . Einzelheiten:

Beachten Sie auch:

Erwin Brandstetter
quelle
Ihr Snippet vor 9.3 löst eine Division durch Null aus, wenn die Tabelle leer ist. Ich wollte eigentlich die Version 9.3+ verwenden, habe aber versehentlich die falsche Version ausgewählt und musste einige Stunden damit verbringen, sie zu reparieren ... Jetzt kann ich die ganze Zeit nicht mehr verschwenden. Ersetzen Sie , unnest(val) / ctdurch , (LEAST(unnest(val), unnest(val) * ct)) / (ct - 1 + sign(ct))und es wirft nicht. Der Grund dafür ist, dass, wenn dies der Fall ctist 0, valersetzt wird durch 0und ctersetzt wird durch 1.
GuiRitter
1
@GuiRitter: Danke für den Hinweis. Ich habe jedoch eine einfachere Lösung angewendet. Auch einige allgemeine Updates währenddessen - aber die Abfrage bleibt gleich.
Erwin Brandstetter
35

Eine Annäherung an die Größe einer Zeile, einschließlich des Inhalts von TOAST , lässt sich leicht ermitteln, indem die Länge der TEXT-Darstellung der gesamten Zeile abgefragt wird:

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

Dies ist eine gute Annäherung an die Anzahl der Bytes, die clientseitig bei der Ausführung abgerufen werden:

SELECT * FROM tablename WHERE primary_key=:value;

... unter der Annahme, dass der Aufrufer der Abfrage Ergebnisse im Textformat anfordert, wie es die meisten Programme tun (binäres Format ist möglich, aber in den meisten Fällen die Mühe nicht wert).

Dieselbe Technik könnte angewendet werden, um die N"größten Zeilen im Text" zu lokalisieren von tablename:

SELECT primary_key, octet_length(t.*::text) FROM tablename AS t
   ORDER BY 2 DESC LIMIT :N;
Daniel Vérité
quelle
Hervorragende Möglichkeit, um bei der Arbeit mit Big Data schnell einige Schätzungen zu erhalten (z. B. liegt der größte Teil der Zeilengröße in Toast-gespeicherten Spalten mit variabler Länge). Gute Idee!
fgblomqvist
Ergebnis sind Bytes?
Akmal Salikhov vor
14

Es gibt ein paar Dinge, die passieren könnten. Im Allgemeinen bezweifle ich, dass die Länge das proximale Problem ist. Ich vermute stattdessen, dass Sie ein Längenproblem haben.

Sie sagen, die Textfelder können bis zu einigen k groß werden. Eine Zeile kann im Hauptspeicher nicht länger als 8 KB sein , und es ist wahrscheinlich, dass Ihre größeren Textfelder GETOASTET wurden oder aus dem Hauptspeicher in einen erweiterten Speicher in separaten Dateien verschoben wurden. Dadurch wird der Hauptspeicher schneller (die Auswahl der ID ist also tatsächlich schneller, weil weniger Plattenseiten zugegriffen werden müssen), aber die Auswahl von * wird langsamer, weil mehr zufällige E / A-Vorgänge ausgeführt werden.

Wenn Ihre gesamte Zeilengröße immer noch deutlich unter 8 KB liegt, können Sie versuchen, die Speichereinstellungen zu ändern. Ich warne Sie jedoch davor, dass beim Einfügen eines übergroßen Attributs in den Hauptspeicher schlimme Dinge passieren können. Berühren Sie dies am besten nicht, wenn dies nicht erforderlich ist, und legen Sie in diesem Fall die entsprechenden Grenzwerte über Check-Einschränkungen fest. Transport ist also wahrscheinlich nicht das einzige. Möglicherweise werden viele, viele Felder sortiert, für die zufällige Lesevorgänge erforderlich sind. Eine große Anzahl zufälliger Lesevorgänge kann auch zu Cache-Fehlern führen, und eine große Menge an erforderlichem Speicher kann erfordern, dass Dinge auf der Festplatte materialisiert werden, und eine große Anzahl breiter Zeilen, falls eine Verknüpfung vorhanden ist (und eine, falls TOAST beteiligt ist), kann kostspieliger sein Verbindungsmuster usw.

Das erste, was ich tun würde, ist die Auswahl weniger Zeilen und sehen, ob das hilft. Wenn das funktioniert, könnten Sie versuchen, dem Server auch mehr RAM hinzuzufügen, aber ich würde anfangen und sehen, wo die Leistung aufgrund von Planänderungen und Cache-Fehlern zuerst nachlässt.

Chris Travers
quelle
4

Verwenden der oben genannten Datenbankobjektgrößenfunktionen :

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

WhiteFire Sondergaard
quelle
sah vielversprechend aus, aber aus irgendeinem Grund funktioniert es in meinem Fall nicht. pg_column_size (tablename.big_column) hat den Wert von pg_column_size (tablename. *)
linqu am