На этой неделе на работе мы часто обсуждали Enum-ы, а также их хранение в БД PostgreSQL-а. После этого мне захотелось порассуждать о том, как было бы здорово, если бы PostgreSQL поддерживал умное хранение строк, применяя оптимизацию String Interning (другие названия — Пул строк, String Compaction).
Итак, какие вообще есть варианты хранения Enum значений в реляционной базе данных. На самом деле, есть как минимум 3 способа хранения значений:
enum Color { blue, red, black }
то можно хранить в базе данных именно эти значения — строки blue, red, black. Плюсы — простая и однозначная сериализация и десериализация. Кроме того, смотря в непосредственно БД, сразу становится понятно, что тут за значение. Минусы — это дорого по объему занимаемой памяти.
А давайте вообще посмотрим, о каких объемах памяти идёт речь. Может быть, это оптимизация на спичках. Может быть, нужно просто хранить строки в случае Enum-значений, и не париться.
Создадим первую тестовую таблицу. Она будет содержать 1 миллион значений — по примерно 10% строк: category1, category2, category3, category4, category5, category6, category7, category8, category9. То есть, у нас миллион значений, где всего 10 уникальных значений Enum.
CREATE TABLE BLOG( CATEGORY CHAR(10) );
А теперь заполним эту таблицу тестовыми данными:
CREATE TABLE TMP_STRINGS( id SERIAL PRIMARY KEY, CATEGORY CHAR(50) ); insert into TMP_STRINGS values (1, 'category1'), (2, 'category2'), (3, 'category3'), (4, 'category4'), (5, 'category5'), (6, 'category6'), (7, 'category7'), (8, 'category8'), (9, 'category9'); insert into blog(category) SELECT category FROM (SELECT floor(random() * 9 + 1)::int as n from generate_series(1,1000000)) AS T INNER JOIN tmp_strings ON T.n = tmp_strings.id;
Получим статистику по этой таблице:
SELECT pg_size_pretty(pg_total_relation_size('"blog"')); SELECT octet_length(t.*::text) FROM blog AS t WHERE category='category1' LIMIT 1; SELECT l.what, l.nr AS "bytes/ct" , CASE WHEN is_size THEN pg_size_pretty(nr) END AS bytes_pretty , CASE WHEN is_size THEN nr / x.ct END AS bytes_per_row FROM ( SELECT min(tableoid) AS tbl -- same as 'public.tbl'::regclass::oid , count(*) AS ct , sum(length(t::text)) AS txt_len -- length in characters FROM blog 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, what, nr);
Итог. Таблица занимает 42 MB места. В среднем, одна строка занимает 14 байт.
Теперь давайте создадим вторую таблицу. Она будет абстолютно такая же, как и первая — только хранится будут SMALLINT значения, а не строки.
CREATE TABLE BLOG( CATEGORY SMALLINT );
Заполним и эту таблицу миллионом чисел от 1 до 9 включительно (аналогично категориям из первого примера):
insert into blog(category) SELECT floor(random() * 9 + 1)::int as n from generate_series(1,1000000);
И получим опять аналогично статистику (запросы не копирую сюда — они аналогичные).
Результаты. Таблица занимает 35 MB места. В среднем, одна строка занимает 3 байта.
С первого взгляда может показаться, что разница — не существенная. Речь идёт примерно о 20% памяти. Однако тут есть несколько Но:
На данный момент в PostgreSQL не реализована оптимизация String Interning. На самом деле, если немного подумать, то сразу становится понятно, что не всё так однозначно, и эта оптимизация может здорово наломать дров:
Возможно, кто-нибудь со стороны и запилит модуль, в котором будет реализован String Interning. И тогда мы уже сможем проверить на реальных данных, есть ли польза от такой оптимизации.
Категории: Программирование