Денормализованное хранение
   данных в 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.id
WHERE
  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.2
Hash 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=6
loops=1)
                    Index Cond: (artist_ids && '{40}'::integer[])
Total runtime: 48455.56 ms
PostgreSQL 9.2
Nested 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 FUNCTION
get_text_array(key text, data text)
RETURNS text[] AS $$
  return JSON.parse(data)[key];
$$ LANGUAGE plv8 IMMUTABLE STRICT;
Пример: функция извлечения числа
CREATE OR REPLACE FUNCTION
get_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 data
FROM film_json
WHERE
  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: 9995049
Total runtime: 2601.073 ms
Пример: создание индекса
CREATE INDEX
price_actual_from_actual_to_idx
ON 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_idx
ON price
USING 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
Спасибо за внимание!

Денормализованное хранение данных в PostgreSQL 9.2 (Александр Коротков)

  • 1.
    Денормализованное хранение данных в PostgreSQL 9.2 Александр Коротков
  • 2.
    Преимущества денормализации • Приправильном использовании – повышение производительности • Упрощение SQL-запросов • При хранении документов – меньше изменений в модели данных
  • 3.
  • 4.
    Хранение данных вмассивах - плюсы • Исчезает JOIN в SQL запросе – быстрее извлекаются данные • При использовании GIN и GiST индексов – быстрый поиск по значению массива • Более простые SQL запросы
  • 5.
    Хранение данных вмассивах - минусы • Нет готовой поддержки со стороны ORM • Большие массивы и частые апдейты – большой overhead из-за MVCC
  • 6.
    Пример: использование промежуточной таблицы SELECT r.* FROM recording r JOIN recording_tag rt ON r.id = rt.recording JOIN tag t ON rt.tag = t.id WHERE t.name = 'jazz';
  • 7.
    План запроса 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
  • 8.
    Пример: использование массива SELECT* FROM recording WHERE tags @> '{jazz}'::text[];
  • 9.
    План запроса Bitmap HeapScan 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
  • 10.
    Массивы в качествевнешних ключей • Давно просили • В 9.2 скорее всего будет (патч в статусе ready for committer)
  • 11.
    Массивы в качествевнешних ключей • Ключевое слово EACH перед именем столбца • Новый действия ON DELETE и ON UPDATE – EACH CASCADE и EACH SET NULL
  • 12.
    Пример 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 );
  • 13.
    Массивы и планировщик •До 9.2 – константные оценки селективности для операторов &&, @>, <@. Планировщик “слеп”. • В 9.2 – сбор специфичной статистики для массивов. Более адекватные планы.
  • 14.
    Пример SELECT * FROM artist_credit ac JOIN recording r ON ac.id = r.artist_credit WHERE ac.artist_ids && '{40}'::int[];
  • 15.
    До PostgreSQL 9.2 HashJoin (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=6 loops=1) Index Cond: (artist_ids && '{40}'::integer[]) Total runtime: 48455.56 ms
  • 16.
    PostgreSQL 9.2 Nested 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
  • 17.
    Как это работает Cобирается следующая статистика: • Самые частые элементы массивов • Их частоты • Гистограмма числа уникальных элементов Можно посмотреть в pg_stats.
  • 18.
  • 19.
    Встроенная поддержка JSONв 9.2 • Тип json и фукнции row_to_json и array_to_json. • Извлекать данные из JSON нечем – остается только собирать в нём ответы.
  • 20.
    Встроенная поддержка JSONв 9.2 • Можно собирать JSON-объект на стороне СУБД • Проще обработка результатов запроса • Меньше размер ответа/число запросов
  • 21.
    Пример 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
  • 22.
    Результат { "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"} ... ] }
  • 23.
    Модуль-расширение PL/v8 • Javascript,как процедурный язык для PostgreSQL • На основе движка v8 от Google • Можно делать любые манипуляции с JSON-данными
  • 24.
    PL/v8 – индексирование •Пишем JS-функцию, которая извлекает то, что нужно: значение или массив • Строим expression index • Делаем поиск по этому expression
  • 25.
    Пример: хранимый документ { "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"] }
  • 26.
    Пример: функция извлечениямассива CREATE OR REPLACE FUNCTION get_text_array(key text, data text) RETURNS text[] AS $$ return JSON.parse(data)[key]; $$ LANGUAGE plv8 IMMUTABLE STRICT;
  • 27.
    Пример: функция извлечениячисла CREATE OR REPLACE FUNCTION get_float(key text, data text) RETURNS float AS $$ return JSON.parse(data)[key]; $$ LANGUAGE plv8 IMMUTABLE STRICT;
  • 28.
    Пример: индексы CREATE INDEXfilm_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));
  • 29.
    Пример: поисковый запрос SELECTdata FROM film_json WHERE get_text_array('actors', data) @> '{MARY KEITEL}'::text[] AND get_float('rental_rate', data) BETWEEN 4.9 AND 5.0;
  • 30.
    Пример: план запроса BitmapHeap 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
  • 31.
    PL/v8 - ограничения •JSON хранится как текст • Каждый раз приходится делать JSON.parse • Нет универсального индекса для документов
  • 32.
  • 33.
    Range types (диапазонныетипы) • Пара, задающая верхнюю и нижнюю границы диапазона • Различные виды интервалов (a,b), (a,b], [a,b), [a,b], (-∞,b), (-∞,b+, (a,+∞), *a,+∞), (-∞;+∞), Ø (“empty”)
  • 34.
    Применение range types •Темпоральные данные (хранение интервала актуальности данных) • Данные с точностью
  • 35.
    Индексирование range types •Btree индекс поддерживает операторы >, <, =. Как правило, не слишком полезен. • GiST поддерживает &&, @>, <@ и т.д. • Можно не хранить данные как range, а просто строить expression index.
  • 36.
    Пример: таблица CREATE TABLEprice ( actual_from timestamp, actual_to timestamp, value float, product_id integer, );
  • 37.
    Пример: запрос SELECT * FROMprice WHERE '2012-03-29'::timestamp >= actual_from AND '2012-03-29'::timestamp < actual_to;
  • 38.
    Пример: план запроса SeqScan on price (cost=0.00..204053.83 rows Filter: (('2012-03-29 00:00:00'::timestamp Rows Removed by Filter: 9995049 Total runtime: 2601.073 ms
  • 39.
    Пример: создание индекса CREATEINDEX price_actual_from_actual_to_idx ON price (actual_from, actual_to);
  • 40.
    Пример: план запроса BitmapHeap 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
  • 41.
    Пример: создание индекса CREATEINDEX price_actual_time_idx ON price USING gist(tsrange(actual_from, actual_to));
  • 42.
    Пример: запрос SELECT * FROMprice WHERE tsrange(actual_from, actual_to) @> '2012-03-29'::timestamp;
  • 43.
    Пример: план запроса BitmapHeap 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
  • 44.
    Перспективы развития • Универсальноеиндексирование для JSON • Сбор статистики для hstore, JSON и т.д. • GiST индексы для массивов разных типов, не только integer
  • 45.