Денормализованное хранение данных в PostgreSQL 9.2
Александр Коротков
Преимущества денормализации
• При правильном использовании – повышение производительности
• Упрощение SQL-запросов• При хранении документов – меньше
изменений в модели данных
Хранение данных в массивах
Хранение данных в массивах - плюсы
• Исчезает JOIN в SQL запросе – быстрее извлекаются данные
• При использовании GIN и GiST индексов – быстрый поиск по значению массива
• Более простые SQL запросы
Хранение данных в массивах - минусы
• Нет готовой поддержки со стороны ORM
• Большие массивы и частые апдейты – большой overhead из-за MVCC
Пример: использованиепромежуточной таблицы
SELECT r.*FROM recording r JOIN recording_tag rt ON r.id =
rt.recording JOIN tag t ON rt.tag = t.idWHERE t.name = 'jazz';
План запросаNested Loop (cost=0.00..377.00 rows=21 width=124) (actual time=0.203. -> Nested Loop (cost=0.00..21.47 rows=21 width=4) (actual time=0.1 -> Index Scan using tag_name_idx on tag t (cost=0.00..8.31 rows= Index Cond: ((name)::text = 'jazz'::text) -> Index Only Scan using recording_tag_tag_recording_idx on recor Index Cond: (tag = t.id) Heap Fetches: 0 -> Index Scan using recording_pkey on recording r (cost=0.00..16.9 Index Cond: (id = rt.recording)Total runtime: 214.631 ms
Пример: использование массива
SELECT *FROM recording WHERE tags @> '{jazz}'::text[];
План запроса
Bitmap Heap Scan on recording (cost=107.43..27372.8 Recheck Cond: (tags @> '{jazz}'::text[]) -> Bitmap Index Scan on recording_tags_idx (cost Index Cond: (tags @> '{jazz}'::text[])Total runtime: 49.235 ms
Массивы в качестве внешних ключей
• Давно просили• В 9.2 скорее всего будет (патч в
статусе ready for committer)
Массивы в качестве внешних ключей
• Ключевое слово EACH перед именем столбца
• Новый действия ON DELETE и ON UPDATE – EACH CASCADE и EACH SET NULL
ПримерCREATE TABLE film ( id serial, title text NOT NULL, ... actor_ids integer[], FOREIGN KEY (EACH actor_ids) REFERENCES actor (id) ON DELETE EACH CASCADE ON UPDATE EACH CASCADE);
Массивы и планировщик• До 9.2 – константные оценки селективности
для операторов &&, @>, <@. Планировщик “слеп”.
• В 9.2 – сбор специфичной статистики для массивов. Более адекватные планы.
ПримерSELECT *FROM artist_credit ac JOIN recording r ON ac.id = r.artist_credit
WHERE ac.artist_ids && '{40}'::int[];
До PostgreSQL 9.2Hash Join (cost=6031.33..369386.51 rows=55296 width=175) (actual time Hash Cond: (r.artist_credit = ac.id) -> Seq Scan on recording r (cost=0.00..293679.83 rows=11059583 wid -> Hash (cost=5996.72..5996.72 rows=2769 width=47) (actual time=0. -> Bitmap Heap Scan on artist_credit ac (cost=69.80..5996.72 Recheck Cond: (artist_ids && '{40}'::integer[]) -> Bitmap Index Scan on artist_credit_artist_ids_idx (cost=0.00..69.11 rows=2769 width=0) (actual time=0.050..0.050 rows=6loops=1) Index Cond: (artist_ids && '{40}'::integer[])Total runtime: 48455.56 ms
PostgreSQL 9.2Nested Loop (cost=16.21..20984.78 rows=559 width=171) (actual -> Bitmap Heap Scan on artist_credit ac (cost=16.21..122.58 Recheck Cond: (artist_ids && '{40}'::integer[]) -> Bitmap Index Scan on artist_credit_artist_ids_idx (cost=0.00..16.21 rows=28 width=0) (actual time=0.024..0.024 rows=6 loops=1) Index Cond: (artist_ids && '{40}'::integer[]) -> Index Scan using recording_artist_credit_idx on recording Index Cond: (artist_credit = ac.id)Total runtime: 6.338 ms
Как это работаетCобирается следующая статистика:
• Самые частые элементы массивов• Их частоты• Гистограмма числа уникальных элементов
Можно посмотреть в pg_stats.
Использование JSON
Встроенная поддержка JSON в 9.2
• Тип json и фукнции row_to_json и array_to_json.• Извлекать данные из JSON нечем –
остается только собирать в нём ответы.
Встроенная поддержка JSON в 9.2
• Можно собирать JSON-объект на стороне СУБД
• Проще обработка результатов запроса• Меньше размер ответа/число запросов
ПримерSELECT row_to_json(x)FROM (SELECT f.*, (SELECT array_agg(a.*) FROM film_actor fa JOIN actor a ON fa.actor_id = a.actor_id WHERE fa.film_id = f.film_id) AS actors FROM film f LIMIT 1) x
Результат{ "film_id":511, "title":"LAWRENCE LOVE", ... "actors":[ {"actor_id":91, "first_name":"CHRISTOPHER“, "last_name":"BERRY“, "last_update":"2006-02-15 09:34:33”}, {"actor_id":101, "first_name":"SUSAN", "last_name":"DAVIS", "last_update":"2006-02-15 09:34:33"} ... ]}
Модуль-расширение PL/v8• Javascript, как процедурный язык для
PostgreSQL• На основе движка v8 от Google• Можно делать любые манипуляции с
JSON-данными
PL/v8 – индексирование• Пишем JS-функцию, которая извлекает
то, что нужно: значение или массив• Строим expression index• Делаем поиск по этому expression
Пример: хранимый документ{ "title":"DOZEN LION", "description":"A Taut Drama of a Cat And a Girl who must Defeat a Frisbee in The Canadian Rockies", "release_year":2006, "rental_rate":4.99, "rating":"NC-17", "actors":["NATALIE HOPKINS","CAMERON WRAY","JADA RYDER","BEN HARRIS","LAURA BRODY","KENNETH HOFFMAN"], "categories":["Documentary"]}
Пример: функция извлечения массиваCREATE OR REPLACE FUNCTIONget_text_array(key text, data text)
RETURNS text[] AS $$ return JSON.parse(data)[key];$$ LANGUAGE plv8 IMMUTABLE STRICT;
Пример: функция извлечения числаCREATE OR REPLACE FUNCTIONget_float(key text, data text)RETURNS float AS $$ return JSON.parse(data)[key];$$ LANGUAGE plv8 IMMUTABLE STRICT;
Пример: индексыCREATE INDEX film_json_actors_idx ON film_json USING gin (get_text_array('actors', data));CREATE INDEX film_json_rental_rate_idx ON film_json(get_float('rental_rate', data));
Пример: поисковый запросSELECT dataFROM film_jsonWHERE get_text_array('actors', data) @> '{MARY KEITEL}'::text[] AND get_float('rental_rate', data) BETWEEN 4.9 AND 5.0;
Пример: план запросаBitmap Heap Scan on film_json (cost=20.92..60.10 rows=13 width Recheck Cond: ((get_text_array('actors'::text, data) @> '{"MA -> BitmapAnd (cost=20.92..20.92 rows=13 width=0) (actual ti -> Bitmap Index Scan on film_json_actors_idx (cost=0.00.. Index Cond: (get_text_array('actors'::text, data) @> -> Bitmap Index Scan on film_json_rental_rate_idx (cost=0 Index Cond: ((get_float('rental_rate'::text, data) >=Total runtime: 0.490 ms
PL/v8 - ограничения• JSON хранится как текст• Каждый раз приходится делать
JSON.parse• Нет универсального индекса для
документов
Диапазонные типы (range types)
Range types (диапазонные типы)• Пара, задающая верхнюю и нижнюю
границы диапазона• Различные виды интервалов (a,b), (a,b],
[a,b), [a,b], (-∞,b), (-∞,b], (a,+∞), [a,+∞), (-∞;+∞), Ø (“empty”)
Применение range types
• Темпоральные данные (хранение интервала актуальности данных)• Данные с точностью
Индексирование range types• Btree индекс поддерживает операторы >, <,
=. Как правило, не слишком полезен.• GiST поддерживает &&, @>, <@ и т.д.• Можно не хранить данные как range, а
просто строить expression index.
Пример: таблицаCREATE TABLE price ( actual_from timestamp, actual_to timestamp, value float, product_id integer,);
Пример: запросSELECT *FROM price WHERE '2012-03-29'::timestamp >= actual_from AND
'2012-03-29'::timestamp < actual_to;
Пример: план запроса
Seq Scan on price (cost=0.00..204053.83 rows Filter: (('2012-03-29 00:00:00'::timestamp Rows Removed by Filter: 9995049Total runtime: 2601.073 ms
Пример: создание индексаCREATE INDEX price_actual_from_actual_to_idxON price (actual_from, actual_to);
Пример: план запроса
Bitmap Heap Scan on price (cost=127071.99..2 Recheck Cond: (('2012-03-29 00:00:00'::time -> Bitmap Index Scan on price_actual_from_ Index Cond: (('2012-03-29 00:00:00'::Total runtime: 566.923 ms
Пример: создание индексаCREATE INDEX price_actual_time_idxON priceUSING gist(tsrange(actual_from, actual_to));
Пример: запросSELECT *FROM price WHERE tsrange(actual_from, actual_to) @> '2012-03-29'::timestamp;
Пример: план запроса
Bitmap Heap Scan on price (cost=464.57..25929.50 rows=10 Recheck Cond: (tsrange(actual_from, actual_to) @> '2012 -> Bitmap Index Scan on price_actual_time_idx (cost=0 Index Cond: (tsrange(actual_from, actual_to) @> 'Total runtime: 80.287 ms
Перспективы развития• Универсальное индексирование для JSON• Сбор статистики для hstore, JSON и т.д.• GiST индексы для массивов разных типов,
не только integer
Спасибо за внимание!
Top Related