Измерьте размер строки таблицы PostgreSQL

83

У меня есть таблица PostgreSQL. select *очень медленно, в то время select idкак приятно и быстро. Я думаю, что это может быть из-за того, что размер строки очень велик, и для транспортировки требуется время, или это может быть какой-то другой фактор.

Мне нужны все поля (или почти все), поэтому выбор только подмножества не является быстрым решением. Выбор полей, которые я хочу, все еще идет медленно.

Вот моя схема таблицы без имен:

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                  | 

Размер текстового поля может быть любым. Но все равно не более нескольких килобайт в худшем случае.

Вопросов

  1. Есть ли что-нибудь об этом, что кричит «сумасшедший неэффективный»?
  2. Есть ли способ измерить размер страницы в командной строке Postgres, чтобы помочь мне отладить это?
Джо
источник
На самом деле ... один из столбцов составляет 11 МБ. Это объяснит это, я думаю. Так есть ли способ сделать, length(*)а не просто length(field)? Я знаю, что это символы, а не байты, но мне нужно только приблизительное значение.
Джо

Ответы:

101

Q2: way to measure page size

PostgreSQL предоставляет ряд функций размера объектов базы данных . Я собрал наиболее интересные из них в этом запросе и добавил некоторые функции доступа к статистике внизу. (Дополнительный модуль pgstattuple предоставляет еще больше полезных функций.)

Это покажет, что разные методы измерения «размера строки» приводят к очень разным результатам. Все зависит от того, что именно вы хотите измерить.

Этот запрос требует Postgres 9.3 или новее . Для более старых версий см. Ниже.

Использование VALUESвыражения в LATERALподзапросе , чтобы избежать написания вычислений для каждой строки.

Замените public.tbl(дважды) на необязательное имя таблицы, дополненное схемой, чтобы получить компактное представление собранной статистики о размере ваших строк. Вы можете обернуть это в функцию plpgsql для многократного использования, передать имя таблицы в качестве параметра и использовать 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);

Результат:

              метрика | байт / кт | bytes_pretty | bytes_per_row
----------------------------------- + ---------- + --- ----------- + ---------------
 core_relation_size | 44138496 | 42 МБ | 91
 visibility_map | 0 | 0 байт | 0
 free_space_map | 32768 | 32 кБ | 0
 table_size_incl_toast | 44179456 | 42 МБ | 91
 indexes_size | 33128448 | 32 МБ | 68
 total_size_incl_toast_and_indexes | 77307904 | 74 МБ | 159
 live_rows_in_text_representation | 29987360 | 29 МБ | 62
 ------------------------------ | | |
 row_count | 483424 | |
 live_tuples | 483424 | |
 dead_tuples | 2677 | |

Для более старых версий (Postgres 9.2 или старше):

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;

Тот же результат.

Q1: anything inefficient?

Вы можете оптимизировать порядок столбцов, чтобы сохранить несколько байтов на строку, которые в настоящее время тратятся на выравнивание:

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                  |

Это экономит от 8 до 18 байт на строку. Я называю это «колонна тетрис» . Подробности:

Также учтите:

Эрвин Брандштеттер
источник
Ваш фрагмент до 9.3 выбрасывает деление на ноль, если таблица пуста. Я на самом деле хотел использовать версию 9.3+, но по ошибке выбрал неправильную и мне пришлось потратить несколько часов на ее исправление ... Теперь я не могу позволить, чтобы все это время было потрачено впустую. Замени , unnest(val) / ctна , (LEAST(unnest(val), unnest(val) * ct)) / (ct - 1 + sign(ct))и не бросит. Обоснованием является то, что, когда ctесть 0, valбудет заменено 0и ctбудет заменено на 1.
GuiRitter
1
@GuiRitter: Спасибо за указание. Я применил более простое решение, хотя. Также есть некоторые общие обновления, но запрос остается прежним.
Эрвин Брандштеттер
35

Аппроксимацию размера строки, включая содержимое TOAST , легко получить, запросив длину представления TEXT всей строки:

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

Это близкое приближение к количеству байтов, которые будут получены на стороне клиента при выполнении:

SELECT * FROM tablename WHERE primary_key=:value;

... при условии, что вызывающий запрос запрашивает результаты в текстовом формате, что и делают большинство программ (двоичный формат возможен, но в большинстве случаев это не стоит проблем).

Тот же метод может быть применен для определения местоположения самых Nбольших в тексте строк tablename:

SELECT primary_key, octet_length(t.*::text) FROM tablename AS t
   ORDER BY 2 DESC LIMIT :N;
Даниэль Верите
источник
Отличный способ быстро получить некоторые оценки при работе с большими данными (например, большая часть размера строки находится в столбцах с тостами переменной длины), хорошая идея!
fgblomqvist
результат байты?
Акмаль Салихов
14

Есть несколько вещей, которые могут происходить. В общем, я сомневаюсь, что длина является проксимальной проблемой. Я подозреваю, что вместо этого у вас есть проблема, связанная с длиной.

Вы говорите, что текстовые поля могут достигать нескольких k. Строка не может превышать 8 КБ в основном хранилище, и вполне вероятно, что ваши большие текстовые поля были TOASTed или перемещены из основного хранилища в расширенное хранилище в отдельных файлах. Это делает ваше основное хранилище быстрее (так что select id на самом деле быстрее, потому что меньше страниц на диске для доступа), но select * становится медленнее, потому что больше случайных операций ввода-вывода.

Если ваш общий размер строки все еще не превышает 8 КБ, вы можете попробовать изменить настройки хранилища. Однако я хотел бы предупредить, что при вставке негабаритного атрибута в основное хранилище могут случиться плохие вещи, поэтому лучше не трогать это, если вам не нужно, и если вы это сделаете, установите соответствующие ограничения с помощью проверочных ограничений. Так что транспортировка вряд ли единственная вещь. Это может сопоставлять много, много полей, которые требуют случайного чтения. Большое количество случайных чтений может также привести к ошибкам в кеше, а большой объем требуемой памяти может потребовать материализации вещей на диске и большого количества широких строк, если присутствует объединение (и есть одно, если задействован TOAST), может потребоваться более дорогостоящее шаблоны соединения и т. д.

Первое, на что я хотел бы обратить внимание, это выбрать меньшее количество строк и посмотреть, поможет ли это. Если это сработает, вы также можете попытаться добавить больше оперативной памяти на сервер, но я бы начал с того, чтобы посмотреть, где начинает падать производительность из-за изменений в плане и отсутствия кэша.

Крис Траверс
источник
4

Использование функций размера объекта базы данных, упомянутых выше:

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

WhiteFire Sondergaard
источник
Выглядело многообещающе, но по какой-то причине это не работает в моем случае. pg_column_size (tablename.big_column) превысил значение pg_column_size (tablename. *)
linqu