СУБД. Лекция 6

СУБД
Навроцкий Артем
Лекция 6

Оптимизация работы с БД

Построение/удаление индексов

База для примера

Построение/удаление индексов

Начальный вариант:

# Query_time: 186.225664  Lock_time: 0.000000
# Rows_sent: 15  Rows_examined: 23 536 988
select m.id, m.title, avg(r.rating)
from movies m
join movie_genres gm on (gm.movie_id = m.id)
join genres  g on (g.id = gm.genre_id)
join movie_tags tm on (tm.movie_id = m.id)
join tags t on (t.id = tm.tag_id)
join ratings r on (r.movie_id = m.id)
where g.name = 'Comedy'
  and t.name = 'Zombie'
group by m.id, m.title
order by avg(r.rating) desc

Построение/удаление индексов

После создания индексов для Foreign Key:

# Query_time: 194.333533  Lock_time: 0.000000
# Rows_sent: 15  Rows_examined: 633 731
select m.id, m.title, avg(r.rating)
from movies m
join movie_genres gm on (gm.movie_id = m.id)
join genres  g on (g.id = gm.genre_id)
join movie_tags tm on (tm.movie_id = m.id)
join tags t on (t.id = tm.tag_id)
join ratings r on (r.movie_id = m.id)
where g.name = 'Comedy'
  and t.name = 'Zombie'
group by m.id, m.title
order by avg(r.rating) desc

Построение/удаление индексов

После создания покрывающего индекса по ratings:

# Query_time: 1.264987  Lock_time: 0.000000
# Rows_sent: 15  Rows_examined: 634 202
select m.id, m.title, avg(r.rating)
from movies m
join movie_genres gm on (gm.movie_id = m.id)
join genres  g on (g.id = gm.genre_id)
join movie_tags tm on (tm.movie_id = m.id)
join tags t on (t.id = tm.tag_id)
join ratings r on (r.movie_id = m.id)
where g.name = 'Comedy'
  and t.name = 'Zombie'
group by m.id, m.title
order by avg(r.rating) desc

Построение/удаление индексов

Итого:

Удалось ускорить в 147.79 раз! Победа?

Что-то не похоже :(

Оптимизируйте доступ к данным:

Типичные ошибки:

Модификация запросов

Что с этим запросом не так?

# Query_time: 1.264987  Lock_time: 0.000000
# Rows_sent: 15  Rows_examined: 634 202
select m.id, m.title, avg(r.rating)
from movies m
join movie_genres gm on (gm.movie_id = m.id)
join genres  g on (g.id = gm.genre_id)
join movie_tags tm on (tm.movie_id = m.id)
join tags t on (t.id = tm.tag_id)
join ratings r on (r.movie_id = m.id)
where g.name = 'Comedy'
  and t.name = 'Zombie'
group by m.id, m.title
order by avg(r.rating) desc

Модификация запросов

Переосмысление запроса:

# Query_time: 14.323418  Rows_examined: 23 618 206 (без индексов)
# Query_time: 25.683016  Rows_examined: 81 575 (с индексами)
# Query_time:  0.178307  Rows_examined: 82 046 (с покрывающим индексом)

select m.id, m.title, m.cnt, avg(r.rating)
from (
  select m.id, m.title, count(*) as cnt
  from movies m
  join movie_genres gm on (gm.movie_id = m.id)
  join genres g on (g.id = gm.genre_id)
  join movie_tags tm on (tm.movie_id = m.id)
  join tags t on (t.id = tm.tag_id)
  where g.name = 'Comedy‘
    and t.name = 'Zombie‘
  group by m.id, m.title
) m
join ratings r on (r.movie_id = m.id)
group by m.id, m.title, m.cnt
order by avg(r.rating) desc

Нормализация

Денормализация

Денормализация
Намеренное приведение структуры базы данных в состояние, не соответствующее критериям нормализации, обычно проводимое с целью ускорения операций чтения из базы за счет добавления избыточных данных.

Нормализация/денормализация

alter table movies
  add column rating_sum float8 default 0 not null,
  add column rating_cnt int default 0 not null;

update movies m join (
  select movie_id, count(*) as rating_cnt, sum(rating) as rating_sum
  from ratings
  group by movie_id
) r on (m.id = r.movie_id)
Set
  m.rating_cnt = r.rating_cnt,
  m.rating_sum = r.rating_sum;

create trigger after insert ...;
create trigger after update ...;
create trigger after delete ...;

Нормализация/денормализация

После денормализации:

# Query_time: 0.015626  Lock_time: 0.000000
# Rows_sent: 15  Rows_examined: 813
select m.id, m.title, case
  when m.rating_cnt > 0
  then m.rating_sum / m.rating_cnt
  else null
end
from movies m
join movie_genres gm on (gm.movie_id = m.id)
join genres g on (g.id = gm.genre_id)
join movie_tags tm on (tm.movie_id = m.id)
join tags t on (t.id = tm.tag_id)
where g.name = 'Comedy'
  and t.name = 'Zombie'
group by m.id, m.title

Уменьшение времени блокировок

Уменьшение времени блокировок за счет избавления от крупных запросов вида «обновить всё».

Разбиение запроса на более мелкие

До:

DELETE FROM work_logs
WHERE created_at > ADDDATE(DATE(NOW()), INTERVAL -90 DAY);

Модификация схемы

До:

BEGIN;
  INSERT INTO spent_daily
  SELECT :day, level, SUM(spent_daily) FROM players
  GROUP BY level;

  UPDATE players SET spent_daily = 0;
END;

UPDATE players SET
  spent_day = spent_day + :delta,
  money = money - :delta
WHERE id = :id;

Модификация схемы

После:

BEGIN;
  INSERT INTO spent_daily
  SELECT :day, level, SUM(CASE
    WHEN spent_day = :day THEN spent_prev
    WHEN spent_day = :day - 1 THEN spent_last
  END) FROM players;
END;

UPDATE players SET
  spent_prev = CASE
    WHEN spent_day = :day THEN spent_prev
    WHEN spent_day = :day - 1 THEN spent_last
    ELSE 0
  END,
  spent_last = CASE
    WHEN spent_day = :day THEN spent_last ELSE 0
  END + :delta,
  spent_day = :day,
  money = money - :delta
WHERE id = :id;

Группировка UPDATE

Массовая замена сгенерированных ресурсов на один с мета-данными.

Группировка UPDATE

Решение «в лоб»:

UPDATE items SET res_id = 73534, level = 1, meta = 1001
WHERE res_id = 40477;
UPDATE items SET res_id = 73534, level = 1, meta = 1201
WHERE res_id = 40478;
UPDATE items SET res_id = 73534, level = 2, meta = 1031
WHERE res_id = 40479;
…
UPDATE items SET res_id = 73534, level = 80, meta = 7641
WHERE res_id = 70477;

Группировка UPDATE

Более красивое решение:

CREATE TEMPORARY TABLE tmp_items (
  res_id INT,
  level SMALLINT,
  meta VARBINARY(64)
) ENGINE = MEMORY;
INSERT INTO items VALUES
  (1, 1001, 40477),
  (1, 1201, 40478),
  …
  (80, 7641, 70477);
UPDATE items I JOIN tmp_items t ON (i.res_id = t.res_id)
SET
  i.res_id = 73534,
  i.level = t.level,
  i.meta = t.meta;

Массовая вставка данных

Загрузка данных через LOAD DATA гораздо быстрее:

load data local infile 'tags.csv'
into table movie_tags
fields
  terminated by ','
  enclosed by '"'
  escaped by ''
lines
  terminated by '\r\n'
ignore 1 lines
(user_id, movie_id, tag, @created)
set created = from_unixtime(@created);

Домашнее задание №3

К следующему рубежному контролю надо будет сделать задание №3.

К рубежному контролю будут допущены только те, кто сделал ДЗ №3.

Навроцкий Артем
@gent: navrotskiy@corp.mail.ru
Спасибо за внимание!