PostgreSQL: практические
примеры оптимизации
SQL-запросов
Фролков Иван
Postgres Professional
Эффективность
● Что такое «эффективный запрос»?
– Быстрый? Но как? Время? Первой строки? Всего запроса?
– Ввод-вывод? Процессор? Блокировки?
● Как мы можем сравнить эффективность?
– Время выполнения
– Количество операций ввода-вывода
Выполнение запроса
● PostgreSQL — Executor
● Можно попросить реальный план выполнения
select * from acc.ledger l where l.reference like 'IN/%'
Index Scan using ledger_pkey on ledger l (cost=0.56..8.58
rows=8879325 width=162) (actual time=0.052..2573.333 rows=8880000
loops=1)
Index Cond: ((reference >= 'IN/'::text) AND (reference <
'IN0'::text))
Filter: (reference ~~ 'IN/%'::text)
Buffers: shared hit=149021 read=168134
Planning time: 0.303 ms
Execution time: 2872.353 ms
Общий принцип
Чем меньше, тем лучше!
Данных
Индексов
Ввода-вывода
Страниц
Блокировок
Latches
Еще случаи
● uuid text — 32 байта
● uuid uuid — 16 байт
● На десятке таких колонок будет совсем интересно
Еще случаи
● uuid text — 32 байта
● uuid uuid — 16 байт
● На десятке таких колонок будет совсем интересно
● Жадничайте!
И еще
● select … from
t1 join t2 join t2
where
? in (t1.col, t2.col, t3.col)
● Такое условие можно вычислить только после соединения.
Индексы
● Все тот же принцип — чем меньше, тем лучше
– Меньше индекс
– Меньше индексов
● Иногда можно и вообще без индексов
Индексы btree
● Индекс — это отсортированная последовательность
● (usr_id)
● (usr_id, added)
– Если оба реально используются, подумайте, нужны ли оба сразу
— оптимизатор выбирает лучший индекс для запроса, а не для
всего приложения
Порядок строк в индексе
● (usr_id) — usr_id равно/больше/меньше
● (usr_id,added) — usr_id равно/больше/меньше
– usr_id равно, added равно/больше/меньше
– НЕ РАБОТАЕТ (почти) -
added равно/больше/меньше
Индексы — LIKE
● Для LIKE индекс используется для поиска по префиксу —
LIKE 'str%'
● Не работает для LIKE '%str'
● Внимание — параметры!
– Тонкий момент в PostgreSQL
Покрытие индексом
● Все колонки есть в индексе
● Меньше обращений к страницам
● Меньше ввод-вывод
● Меньше latches/buffer pin
● Больше индексов/больше индекс
Пример
create table ios(
id int primary key,
val text)
insert into ios select n, repeat('X', n%100) from
generate_series(1,1000000) as gs(n)
explain(analyze, verbose,buffers)
select count(val) from ios where id between 1 and 100000
explain(analyze, verbose,buffers)
select count(id) from ios where id between 1 and 100000
План 1
Aggregate (cost=3998.45..3998.46 rows=1 width=8)
(actual time=22.241..22.242 rows=1 loops=1)
Output: count(val)
Buffers: shared hit=816
-> Index Scan using ios_pkey on public.ios
…
Output: id, val
…
Buffers: shared hit=816
План Бэ
Aggregate (cost=3347.30..3347.31 rows=1 width=8)
(actual time=16.804..16.804 rows=1 loops=1)
Buffers: shared hit=277
-> Index Only Scan using ios_pkey on public.ios
…
Heap Fetches: 0
Buffers: shared hit=277
Сравнение при параллельном выполнении
● 8 клиентов
– Обычный доступ — 220.709118 tps
– Index-only scan - 336.434901 tps
Покрытие индексом
● PostgresPro — INCLUDING
● Покрытие индексом — предпоследний способ повысить
производительность
● Почему плохо
– На каждый запрос делать индекс — это ж сколько их будет?
– Что делать, если запрос поменялся?
Методы соединения
select * from first, second
where first.key=second.key
Методы соединения
select * from first, second
where first.key=second.key
select * from first, second
where first.key<>second.key
Методы соединения
select * from first, second
where first.key=second.key
select * from first, second
where first.key<>second.key
select * from first, second
where exists(select * from third where
third.first_key=first.key and
third.second_key=second.key)
Методы соединения
● Nested loops
– for i in first_table
● For j in second_table where second_table.i=i
проверяем условия и формируем строку
Методы соединения
● Nested loops
– for i in first_table
● For j in second_table where second_table.i=i
проверяем условия и формируем строку
● Hash join
– Строим хэш-таблицу из first_table
● for j in second_table
if key_exists(hash(second_table.j))
–проверяем условия и формируем строку
– Что делать, если таблица не помещается в память?
Методы соединения
●
Nested loops
– for i in first_table
●
For j in second_table where second_table.i=i
проверяем условия и формируем строку
● Hash join
– Строим хэш-таблицу из first_table
●
for j in second_table
if key_exists(hash(second_table.j))
–проверяем условия и формируем строку
●
Merge join
–Сливаем две отсортированных first_table & second_table
●
проверяем условия и формируем строку
Методы соединения
● Nested loops
– За
● Очень дешевый
● Очень быстрый на небольших объемах
● Не требует много памяти
● Идеален для молниеносных запросов
● Единственный умеет соединения не только по равенству
– Против
● Плохо работает для больших объемов данных
Методы соединения
● Hash join
– За
●
Не нужен индекс
● Относительно быстрый
● Может быть использован для FULL OUTER JOIN
– Против
● Любит память
● Соединение только по равенству
● Не любит много значений в колонках соединения
● Велико время получения первой строки
Методы соединения
● Merge join
– За
● Быстрый на больших и малых объемах
● Не требует много памяти
● Умеет OUTER JOIN
● Подходит для соединения более чем двух таблиц
– Против
● Требует отсортированные потоки данных, что подразумевает или индекс, или
сортировку
● Соединение только по равенству
Про Postgres
● Не умеет full outer join с соединением не по равенству
● Вот только что-то никто не жаловался :-)
Статистика
● В PostgreSQL — pg_statistics или на ее основе
представление pg_stats
● Статистика — ключевой фактор для работы оптимизатора
● Проблемы — пары-тройки-четверки колонок
● Oracle — умеет. А вот PostgreSQL — нет :-(
Типовые проблемы
● Плохая схема БД
– Объемы!
● Лишние данные
● Лишние индексы
● Отсутствие нужных индексов
● Неверные типы
– Необходимость писать сложные запросы
● Бездумное использование ОРМ
Бездумное использование ОРМ
● Вообще говоря, я его не люблю
– Но мало ли что я не люблю. А народу вот нравится
●
Типовой запрос:
– select distinct <от десятков до сотен колонок>
from table1 left outer join table2 on …
left outer join table3 on …
left outer join table4 on …
where table4.col='value'
order by table1.id
limit 100
offset 20000
ОРМ-запрос. Что тут плохо
● DISTINCT
– Если вы не можете точно сказать, зачем вы используете
DISTINCT, то у вас проблемы
ОРМ-запрос. Что тут плохо
● DISTINCT
– Если вы не можете точно сказать, зачем вы используете
DISTINCT, то у вас проблемы
● LEFT OUTER JOIN
– Бьет по рукам оптимизатору, строго задавая порядок
соединения
– Более того, условия во WHERE делают внешнее соединение
ненужным
ОРМ-запрос. Что тут плохо
● DISTINCT
– Если вы не можете точно сказать, зачем вы используете
DISTINCT, то у вас проблемы
● LEFT OUTER JOIN
– Бьет по рукам оптимизатору, строго задавая порядок соединения
– Более того, условия во WHERE делают внешнее соединение
ненужным
● LIMIT/OFFSET почти всегда плохо
Что делать?
● Четко определиться, какую бизнес-задачу решает запрос.
Возможно, после этого необходимость в нем отпадет
● Разобраться с ОРМ
– Выбирать только то, что нужно
– Постараться перейти к INNER JOIN
– Постараться избавиться от LIMIT/OFFSET
Выводы
● Чем меньше, тем лучше
● Знайте ваши данные

PostgreSQL: практические примеры оптимизации SQL-запросов / Иван Фролков (Postgres Professional)

  • 1.
  • 2.
    Эффективность ● Что такое«эффективный запрос»? – Быстрый? Но как? Время? Первой строки? Всего запроса? – Ввод-вывод? Процессор? Блокировки? ● Как мы можем сравнить эффективность? – Время выполнения – Количество операций ввода-вывода
  • 3.
    Выполнение запроса ● PostgreSQL— Executor ● Можно попросить реальный план выполнения select * from acc.ledger l where l.reference like 'IN/%' Index Scan using ledger_pkey on ledger l (cost=0.56..8.58 rows=8879325 width=162) (actual time=0.052..2573.333 rows=8880000 loops=1) Index Cond: ((reference >= 'IN/'::text) AND (reference < 'IN0'::text)) Filter: (reference ~~ 'IN/%'::text) Buffers: shared hit=149021 read=168134 Planning time: 0.303 ms Execution time: 2872.353 ms
  • 4.
    Общий принцип Чем меньше,тем лучше! Данных Индексов Ввода-вывода Страниц Блокировок Latches
  • 5.
    Еще случаи ● uuidtext — 32 байта ● uuid uuid — 16 байт ● На десятке таких колонок будет совсем интересно
  • 6.
    Еще случаи ● uuidtext — 32 байта ● uuid uuid — 16 байт ● На десятке таких колонок будет совсем интересно ● Жадничайте!
  • 7.
    И еще ● select… from t1 join t2 join t2 where ? in (t1.col, t2.col, t3.col) ● Такое условие можно вычислить только после соединения.
  • 8.
    Индексы ● Все тотже принцип — чем меньше, тем лучше – Меньше индекс – Меньше индексов ● Иногда можно и вообще без индексов
  • 9.
    Индексы btree ● Индекс— это отсортированная последовательность ● (usr_id) ● (usr_id, added) – Если оба реально используются, подумайте, нужны ли оба сразу — оптимизатор выбирает лучший индекс для запроса, а не для всего приложения
  • 10.
    Порядок строк виндексе ● (usr_id) — usr_id равно/больше/меньше ● (usr_id,added) — usr_id равно/больше/меньше – usr_id равно, added равно/больше/меньше – НЕ РАБОТАЕТ (почти) - added равно/больше/меньше
  • 11.
    Индексы — LIKE ●Для LIKE индекс используется для поиска по префиксу — LIKE 'str%' ● Не работает для LIKE '%str' ● Внимание — параметры! – Тонкий момент в PostgreSQL
  • 12.
    Покрытие индексом ● Всеколонки есть в индексе ● Меньше обращений к страницам ● Меньше ввод-вывод ● Меньше latches/buffer pin ● Больше индексов/больше индекс
  • 13.
    Пример create table ios( idint primary key, val text) insert into ios select n, repeat('X', n%100) from generate_series(1,1000000) as gs(n) explain(analyze, verbose,buffers) select count(val) from ios where id between 1 and 100000 explain(analyze, verbose,buffers) select count(id) from ios where id between 1 and 100000
  • 14.
    План 1 Aggregate (cost=3998.45..3998.46rows=1 width=8) (actual time=22.241..22.242 rows=1 loops=1) Output: count(val) Buffers: shared hit=816 -> Index Scan using ios_pkey on public.ios … Output: id, val … Buffers: shared hit=816
  • 15.
    План Бэ Aggregate (cost=3347.30..3347.31rows=1 width=8) (actual time=16.804..16.804 rows=1 loops=1) Buffers: shared hit=277 -> Index Only Scan using ios_pkey on public.ios … Heap Fetches: 0 Buffers: shared hit=277
  • 16.
    Сравнение при параллельномвыполнении ● 8 клиентов – Обычный доступ — 220.709118 tps – Index-only scan - 336.434901 tps
  • 17.
    Покрытие индексом ● PostgresPro— INCLUDING ● Покрытие индексом — предпоследний способ повысить производительность ● Почему плохо – На каждый запрос делать индекс — это ж сколько их будет? – Что делать, если запрос поменялся?
  • 18.
    Методы соединения select *from first, second where first.key=second.key
  • 19.
    Методы соединения select *from first, second where first.key=second.key select * from first, second where first.key<>second.key
  • 20.
    Методы соединения select *from first, second where first.key=second.key select * from first, second where first.key<>second.key select * from first, second where exists(select * from third where third.first_key=first.key and third.second_key=second.key)
  • 21.
    Методы соединения ● Nestedloops – for i in first_table ● For j in second_table where second_table.i=i проверяем условия и формируем строку
  • 22.
    Методы соединения ● Nestedloops – for i in first_table ● For j in second_table where second_table.i=i проверяем условия и формируем строку ● Hash join – Строим хэш-таблицу из first_table ● for j in second_table if key_exists(hash(second_table.j)) –проверяем условия и формируем строку – Что делать, если таблица не помещается в память?
  • 23.
    Методы соединения ● Nested loops –for i in first_table ● For j in second_table where second_table.i=i проверяем условия и формируем строку ● Hash join – Строим хэш-таблицу из first_table ● for j in second_table if key_exists(hash(second_table.j)) –проверяем условия и формируем строку ● Merge join –Сливаем две отсортированных first_table & second_table ● проверяем условия и формируем строку
  • 24.
    Методы соединения ● Nestedloops – За ● Очень дешевый ● Очень быстрый на небольших объемах ● Не требует много памяти ● Идеален для молниеносных запросов ● Единственный умеет соединения не только по равенству – Против ● Плохо работает для больших объемов данных
  • 25.
    Методы соединения ● Hashjoin – За ● Не нужен индекс ● Относительно быстрый ● Может быть использован для FULL OUTER JOIN – Против ● Любит память ● Соединение только по равенству ● Не любит много значений в колонках соединения ● Велико время получения первой строки
  • 26.
    Методы соединения ● Mergejoin – За ● Быстрый на больших и малых объемах ● Не требует много памяти ● Умеет OUTER JOIN ● Подходит для соединения более чем двух таблиц – Против ● Требует отсортированные потоки данных, что подразумевает или индекс, или сортировку ● Соединение только по равенству
  • 27.
    Про Postgres ● Неумеет full outer join с соединением не по равенству ● Вот только что-то никто не жаловался :-)
  • 28.
    Статистика ● В PostgreSQL— pg_statistics или на ее основе представление pg_stats ● Статистика — ключевой фактор для работы оптимизатора ● Проблемы — пары-тройки-четверки колонок ● Oracle — умеет. А вот PostgreSQL — нет :-(
  • 29.
    Типовые проблемы ● Плохаясхема БД – Объемы! ● Лишние данные ● Лишние индексы ● Отсутствие нужных индексов ● Неверные типы – Необходимость писать сложные запросы ● Бездумное использование ОРМ
  • 30.
    Бездумное использование ОРМ ●Вообще говоря, я его не люблю – Но мало ли что я не люблю. А народу вот нравится ● Типовой запрос: – select distinct <от десятков до сотен колонок> from table1 left outer join table2 on … left outer join table3 on … left outer join table4 on … where table4.col='value' order by table1.id limit 100 offset 20000
  • 31.
    ОРМ-запрос. Что тутплохо ● DISTINCT – Если вы не можете точно сказать, зачем вы используете DISTINCT, то у вас проблемы
  • 32.
    ОРМ-запрос. Что тутплохо ● DISTINCT – Если вы не можете точно сказать, зачем вы используете DISTINCT, то у вас проблемы ● LEFT OUTER JOIN – Бьет по рукам оптимизатору, строго задавая порядок соединения – Более того, условия во WHERE делают внешнее соединение ненужным
  • 33.
    ОРМ-запрос. Что тутплохо ● DISTINCT – Если вы не можете точно сказать, зачем вы используете DISTINCT, то у вас проблемы ● LEFT OUTER JOIN – Бьет по рукам оптимизатору, строго задавая порядок соединения – Более того, условия во WHERE делают внешнее соединение ненужным ● LIMIT/OFFSET почти всегда плохо
  • 34.
    Что делать? ● Четкоопределиться, какую бизнес-задачу решает запрос. Возможно, после этого необходимость в нем отпадет ● Разобраться с ОРМ – Выбирать только то, что нужно – Постараться перейти к INNER JOIN – Постараться избавиться от LIMIT/OFFSET
  • 35.
    Выводы ● Чем меньше,тем лучше ● Знайте ваши данные