2. МЫ РАССМОТРИМ
● Основные моменты работы с РБД
● Что такое EXPLAIN и как с ним работать
● Индексы и их особенности
● Лайфхаки и хитрости
3. Основы работы с РБД
● Запись в таблице должна соответствовать объекту
● Правильно выбрать уровень нормализации
● Уделить немало времени на проектирование
● Структура БД должна быть гибкой
● Простота в поддержке жизненого цикла БД
● Правильно расставлять индексы,
не создавать бардак
● Строить запросы не выбирая все подряд (*),
только необходимое
● Приступать к оптимизации, когда действительно это
требуется
4. ORM vs SQl
●
ORM — удобно?
●
ORM — быстро?
Total runtime: (~)117.092 ms*
JAVA
@ManyToOne
@JoinColumn(name = "CATEGORY_ID")
public Category getCategory() {
return category;
}
PHP
$customers = Customer::find()
->where(['status' => TRUE])
->orderBy('id')
->limit(100, 10000);
* Подробный пример мы разберем ниже
☑
5. ORM vs SQL
●
SQL — удобно?
●
SQL — быстро?
Total runtime: (~)11.926 ms
SELECT * FROM (
SELECT * FROM posts
WHERE author = 'nick'
ORDER BY posts.publish
DESC LIMIT 10
) as posts
JOIN post_category
ON posts.category_id = category.id
ORDER BY posts.publish DESC LIMIT 10;
☒
* Подробный пример мы разберем ниже
6. Еще об ORM
● Позволяет представить запись в БД в виде объекта
● Удобнее понимать и легче писать чем SQL
● Поддержка и изменения не вызывают трудностей
● Сложные запросы иногда невозможно написать
● Трудно оптимизировать
● Иногда строится запрос, который не использует
индексы
7. А что с SQL?
● Сложные выборки, запросы
● Возможности оптимизации
● Функции и различные возможности SQL
● HighLoad — однозначно SQL (узкое место)
● Запрос с использованием индексов не всегда быстрый
● Замусоривает код (JAVA и многострочные литералы)
● Нет проверок на этапе компиляции
9. ANALYZE
ANALYZE считывается определённое количество
строк таблицы в базе данных, выбранных случайным
образом, и сохраняет результаты в системном
каталоге pg_statistic.
Затем планировщик запросов будет использовать эту
статистику для выбора эффективных планов
запросов.
=> ANALYZE VERBOSE;
WARNING: skipping "pg_statistic" --- only superuser or database owner can analyze it
WARNING: skipping "pg_type" --- only superuser or database owner can analyze it
INFO: analyzing "public.test"
INFO: "test": scanned 16669 of 16669 pages, containing 2000200 live rows
and 0 dead rows; 30000 rows in sample, 2000200 estimated total rows
10. VACUUM
● Очистка места (помечание), занимаемое
«мертвыми» кортежами
● По-умолчанию очищает все таблицы доступные
пользователю
● Без опции FULL может работать параллельно,
т. к. не требует исключительной блокировки
● With FULL работает медленно, требует блокировки
и возвращает освобожденное место операционной
системе.
● Autovacuum — демон очистки (VACUUM+ANALYZE)
● ! table bloating
12. Таблица TEST
=> CREATE TABLE test (id integer, text text);
=> INSERT INTO test
SELECT id, md5(random()::text)
FROM generate_series(1, 1000000) AS id;
=> d test
Table "public.test"
Column | Type | Modifiers
----------------+------------+-----------
column_1 | integer |
column_2 | text |
13. => EXPLAIN SELECT * FROM test;
● Cost — у.е. для оценки затратности
операции.
1ое значение — затраты доступа к 1й строке
2ое значение — затраты для доступа ко
всем строкам.
● Rows — (~) количество строк при вызове
Seq Scan к этой таблице
● With — длина строки в байтах
14. Всё, что мы видели выше в выводе команды EXPLAIN —
ожидания планировщика.
=> EXPLAIN (ANALYZE) SELECT * FROM test;
● Actual time — реальное время в миллисекундах для
1ой и всех строк
● Rows — реальное количество строк полученных
● Loops — количество выполнений данной операции
● Plannig time — время выполнения EXPLAIN
● Execution time — общее время выполнения
● Heap Fetches — число реальных обращений к таблице
!!! DANGER !!!
EXPLAIN (ANALYZE) исполняет команды
15. => EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM test;
● Buffers: shared read — количество блоков
считанных с диска;
● Buffers: shared hit — количество блоков,
считанных из кэша PostgreSQL.
CACHE
16. WHERE
=> EXPLAIN (ANALYZE)
SELECT * FROM test WHERE id
between 1500 and 1550;
● Индексов нет поэтому Seq Scan
● Каждая строка сравнивается с:
Filter: ((id >= 1500) AND (id <= 1550))
● Cost увеличилось
● Rows уменьшилось до ождаемого количества
Execution time: 222.979 ms
17. Index Scan
=> CREATE INDEX ON test(id);
=> EXPLAIN (ANALYZE)
SELECT * FROM test WHERE id
between 1500 and 1550;
● Теперь Index Scan using test_id_idx on test;
● Index Cond: ((id >= 1500) AND (id <= 1550))
Execution time: 0.127 ms
18. Seq Scan
A C D
1) С < 10
2) С < 10
3) С < 10
С < 10
20. ИНДЕКСЫ
● Но что будет, если поменять условие
=> EXPLAIN (ANALYZE)
SELECT * FROM test WHERE id > 1550;
● Теперь Seq Scan on test
● Пришлось прочитать все строки, кроме
первых 1500..
● Время увеличилось, что не удивительно
Execution time: 678.852 ms
21. ИНДЕКСЫ
● А если выключить Seq Scan
=> SET enable_seqscan TO off;
● Теперь Index Scan using test_id_idx on test
● Но время запроса стало еще больше
Execution time: 749.952 ms
● И стоимость cost также увеличилась
Планировщик не дурак =)
22. Про индексы
● Индекс это дополнительная структура
данных (не SQL)
● Индексы требуют затраты на поддержание
● Замедляют обновление
● Замедляют репликацию
● Малая селективность — неэфективно
Индексы не панацея!
23. Index Only Scan
● EXPLAIN (ANALYZE)
SELECT id FROM test WHERE id < 450;
● Index Only Scan using test_id_idx on test
● Выбираем только поле id,
чтобы включить IOS
● Скорость очень большая
Execution time: 0.659 ms
25. ИНДЕКСЫ ПО ТЕКСТУ
EXPLAIN (ANALYZE)
SELECT * FROM test WHERE text LIKE 'ab%';
● Seq Scan
After CREATE INDEX ON test(text);
● Также Seq Scan (211 ms), потому что UTF-8!
● Нужно использовать класс оператора
text_pattern_ops
CREATE INDEX ON test(text text_pattern_ops);
26. ИНДЕКСЫ ПО ТЕКСТУ
EXPLAIN (ANALYZE)
SELECT * FROM test WHERE text LIKE 'ab%';
● Bitmap Index Scan on test_text_idx1
● Сравниваем
Index Cond: (
(text ~>=~ 'ab'::text) AND (text ~<~ 'ac'::text)
)
● Далее Bitmap Heap Scan on test
проверяет существуют ли записи на самом
деле
28. Создание индексов
● Требуют блокировки при создании
● CONCURRENTLY создает в фоне, но долго (требует
2 прохода)
● Можно и нужно мониторить неиспользуемые
индексы (расходуются рессурсы и время)
● Можно находить дубликаты индексов
● Можно строить индексы по функциям, но
необходимо точное её повторение при запросе
● ! index bloating
29. Когда создавать индексы?
CREATE TABLE test5 (id integer PRIMARY KEY, v float8);
ACREATE INDEX test5_v_idx ON test5(v);
INSERT INTO test5
(SELECT id, random() FROM generate_series(1,1000000) id);
CREATE TABLE test5 (id integer PRIMARY KEY, v float8);
BINSERT INTO test5
(SELECT id, random() FROM generate_series(1,1000000) id);
CREATE INDEX test5_v_idx ON test5(v);
30. Когда создавать индексы?
CREATE TABLE a (id integer PRIMARY KEY, v float8); 1,991 ms
CREATE INDEX a_v_idx ON a(v); 0,506 ms
INSERT INTO a
(SELECT id, random() FROM
generate_series(1,1000000) id);
4909,127 ms
A = Total: 4911 ms
CREATE TABLE b (id integer PRIMARY KEY, v float8); 1,990 ms
INSERT INTO b
(SELECT id, random() FROM
generate_series(1,1000000) id);
938,852 ms
CREATE INDEX b_v_idx ON b(v); 1195,492 ms
B = Total: 2136 ms
32. Мониторим неиспользуемые индексы
SELECT schemaname || '.' || relname AS table, indexrelname AS index,
pg_size_pretty(pg_relation_size(i.indexrelid)) AS index_size, idx_scan as
index_scans
FROM pg_stat_user_indexes ui
JOIN pg_index i ON ui.indexrelid = i.indexrelid
WHERE NOT indisunique AND idx_scan < 50 AND pg_relation_size(relid) > 5 * 819
ORDER BY pg_relation_size(i.indexrelid) / nullif(idx_scan, 0) DESC NULLS FIRST,
pg_relation_size(i.indexrelid) DESC;
table | index | index_size | index_scans
---------------------+---------------------------+---------------+-------------
public.test | test_text_idx | 56 MB | 0
public.test5 | test5_v_idx | 28 MB | 0
public.test6 | test6_v_idx | 21 MB | 0
public.test | test_text_idx1 | 56 MB | 3
public.test | test_id_idx | 21 MB | 36
33. Ищем дубликаты индексов
lk=> SELECT pg_size_pretty(SUM(pg_relation_size(idx))::BIGINT) AS SIZE,
(array_agg(idx))[1] AS idx1, (array_agg(idx))[2] AS idx2,
(array_agg(idx))[3] AS idx3, (array_agg(idx))[4] AS idx4
FROM ( SELECT indexrelid::regclass AS idx,
(indrelid::text ||E'n'|| indclass::text ||E'n'|| indkey::text ||E'n'||
COALESCE(indexprs::text,'')||E'n' || COALESCE(indpred::text,'')) AS KEY
FROM pg_index) sub
GROUP BY KEY HAVING COUNT(*)>1
ORDER BY SUM(pg_relation_size(idx)) DESC;
size | idx1 | idx2 | idx3 | idx4
---------+-----------------------+----------------------+------+------
32 kB | blocks_id_idx | blocks_id_idx1 | |
32 kB | blocks_type_idx1 | blocks_type_idx | |
32 kB | primary_key | ids | |
34. Оптимизация OFFSET
Ситуация:
SELECT …
FROM table1
JOIN table2 using (table2id)
JOIN table3 using (table3id)
WHERE
набор условий ТОЛЬКО по table1
Order by (набор полей table1) LIMIT ... OFFSET ...
Важно: сработает если соблюдается условие, что
логика выборки и offset реализуется в table1, а также
что присоединенные данные из таблиц table2 и table3
на запрос не влияют.
35. EXPLAIN ANALYZE
SELECT * FROM test
JOIN vals ON vals.test_id = test.id
WHERE val between 150 AND 9500
LIMIT 5 OFFSET 5000;
Execution time: 283.471 ms
EXPLAIN ANALYZE
SELECT * FROM ( SELECT * FROM test
WHERE val between 150 AND 9500
LIMIT 5 OFFSET 5000) AS test
JOIN vals ON vals.test_id = test.id;
Execution time: 4.079 ms
36. Оптимизация COUNT(*)
=> EXPLAIN ANALYZE SELECT count(*) FROM
cache_customers_rates;
Execution time: 18632.959 ms
=> EXPLAIN ANALYZE SELECT
(reltuples)::numeric FROM pg_class r WHERE
relkind='r' AND relname='cache_customers_rates';
Execution time: 0.079 ms
37. Получение строк в виде ROW
=> SELECT * FROM tasks;
id | type | status | params | out. | exc.
----+---------------+---------+------------------+------+-----
1 | refill_cache | new | {"threads":-1} | |
2 | refill_cache | new | {"threads":-1} | |
=> SELECT tasks FROM tasks;
tasks
--------------------------------------------------------------------
( 1, refill_cache, new, "{""threads"":-1}", "", "" )
( 2, refill_cache, new, "{""threads"":-1}", "", "" )
38. Получение строк в виде JSON
=> SELECT row_to_json(tasks) FROM tasks;
row_to_json
-----------------------------------------------------------------
{
"id":1,
"type":"refill_cache",
"status":"new",
"params":"{"threads":-1}",
"output":"",
"exception":""
}
39. Выбранные поля в виде JSON
=> SELECT row_to_json( t ) FROM ( SELECT id,
type FROM tasks) AS t;
row_to_json
------------------------------------------------
{ "id":1, "type":"refill_cache" }
{ "id":1, "type":"refill_cache" }