View
136
Download
4
Category
Preview:
Citation preview
НАРОДНЫЕ СРЕДСТВА ОПТИМИЗАЦИИ ЗАПРОСОВ
В PostgreSQL
Писарев Николай
МЫ РАССМОТРИМ
● Основные моменты работы с РБД● Что такое EXPLAIN и как с ним работать● Индексы и их особенности● Лайфхаки и хитрости
Основы работы с РБД
● Запись в таблице должна соответствовать объекту
● Правильно выбрать уровень нормализации
● Уделить немало времени на проектирование
● Структура БД должна быть гибкой
● Простота в поддержке жизненого цикла БД
● Правильно расставлять индексы, не создавать бардак
● Строить запросы не выбирая все подряд (*), только необходимое
● Приступать к оптимизации, когда действительно это требуется
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);
* Подробный пример мы разберем ниже
☑
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;
☒
* Подробный пример мы разберем ниже
Еще об ORM
● Позволяет представить запись в БД в виде объекта● Удобнее понимать и легче писать чем SQL● Поддержка и изменения не вызывают трудностей
● Сложные запросы иногда невозможно написать● Трудно оптимизировать● Иногда строится запрос, который не использует
индексы
А что с SQL?
● Сложные выборки, запросы● Возможности оптимизации● Функции и различные возможности SQL● HighLoad — однозначно SQL (узкое место)
● Запрос с использованием индексов не всегда быстрый● Замусоривает код (JAVA и многострочные литералы)● Нет проверок на этапе компиляции
ANALYZE
ANALYZE считывается определённое количество строк таблицы в базе данных, выбранных случайным образом, и сохраняет результаты в системном каталоге pg_statistic.
Затем планировщик запросов будет использовать эту статистику для выбора эффективных планов запросов.
=> ANALYZE VERBOSE;WARNING: skipping "pg_statistic" --- only superuser or database owner can analyze itWARNING: skipping "pg_type" --- only superuser or database owner can analyze itINFO: 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
VACUUM
● Очистка места (помечание), занимаемое «мертвыми» кортежами
● По-умолчанию очищает все таблицы доступные пользователю
● Без опции FULL может работать параллельно, т. к. не требует исключительной блокировки
● With FULL работает медленно, требует блокировки и возвращает освобожденное место операционной системе.
● Autovacuum — демон очистки (VACUUM+ANALYZE)● ! table bloating
Таблица 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 |
=> EXPLAIN SELECT * FROM test;● Cost — у.е. для оценки затратности
операции. 1ое значение — затраты доступа к 1й строке2ое значение — затраты для доступа ко всем строкам.
● Rows — (~) количество строк при вызове Seq Scan к этой таблице
● With — длина строки в байтах
Всё, что мы видели выше в выводе команды EXPLAIN — ожидания планировщика.
=> EXPLAIN (ANALYZE) SELECT * FROM test;● Actual time — реальное время в миллисекундах для
1ой и всех строк● Rows — реальное количество строк полученных● Loops — количество выполнений данной операции● Plannig time — время выполнения EXPLAIN● Execution time — общее время выполнения● Heap Fetches — число реальных обращений к таблице
!!! DANGER !!!
EXPLAIN (ANALYZE) исполняет команды
=> EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM test;
● Buffers: shared read — количество блоков считанных с диска;
● Buffers: shared hit — количество блоков, считанных из кэша PostgreSQL.
CACHE
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
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
Seq Scan
A C D
1) С < 10
2) С < 10
3) С < 10
С < 10
Index Scan
A C D
С < 10
C
ИНДЕКСЫ
● Но что будет, если поменять условие
=> EXPLAIN (ANALYZE) SELECT * FROM test WHERE id > 1550;
● Теперь Seq Scan on test● Пришлось прочитать все строки, кроме
первых 1500..● Время увеличилось, что не удивительно
Execution time: 678.852 ms
ИНДЕКСЫ
● А если выключить Seq Scan
=> SET enable_seqscan TO off;● Теперь Index Scan using test_id_idx on test● Но время запроса стало еще больше
Execution time: 749.952 ms● И стоимость cost также увеличилась
Планировщик не дурак =)
Про индексы
● Индекс это дополнительная структура данных (не SQL)
● Индексы требуют затраты на поддержание● Замедляют обновление● Замедляют репликацию● Малая селективность — неэфективно
Индексы не панацея!
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
Index Only Scan
A C D
С < 10
C
VisabilityMAP
ИНДЕКСЫ ПО ТЕКСТУ
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);
ИНДЕКСЫ ПО ТЕКСТУ
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
проверяет существуют ли записи на самом деле
Bitmap Index Scan
A C D
С < 10
C
1
1 1
1
1 0
Создание индексов
● Требуют блокировки при создании● CONCURRENTLY создает в фоне, но долго (требует
2 прохода)● Можно и нужно мониторить неиспользуемые
индексы (расходуются рессурсы и время)● Можно находить дубликаты индексов● Можно строить индексы по функциям, но
необходимо точное её повторение при запросе● ! index bloating
Когда создавать индексы?
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);
Когда создавать индексы?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
Lifehack Show
Мониторим неиспользуемые индексы
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
Ищем дубликаты индексов
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 | |
Оптимизация OFFSET
Ситуация:
SELECT …FROM table1JOIN table2 using (table2id)JOIN table3 using (table3id)WHEREнабор условий ТОЛЬКО по table1Order by (набор полей table1) LIMIT ... OFFSET ...
Важно: сработает если соблюдается условие, что логика выборки и offset реализуется в table1, а также что присоединенные данные из таблиц table2 и table3 на запрос не влияют.
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
Оптимизация 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
Получение строк в виде 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}", "", "" )
Получение строк в виде JSON
=> SELECT row_to_json(tasks) FROM tasks; row_to_json-----------------------------------------------------------------{ "id":1, "type":"refill_cache", "status":"new", "params":"{\"threads\":-1}", "output":"", "exception":"" }
Выбранные поля в виде 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" }
Данные в JSON ARRAY
=> SELECT array_to_json( array_agg( row_to_json(tasks) ) ) FROM tasks;
[ { "id":1, "type":"refill_cache", "status":"new", "params":"{\"threads\":-1}", "output":"", "exception":"" }, { "id":2, "type":"refill_cache", "status":"new", "params":"{\"threads\":-1}", "output":"", "exception":"" } ]
OUTPUT to FILE
=> \o tasks.json
=> SELECT array_to_json( array_agg( row_to_json(tasks) ) ) FROM tasks;
=> \o
# cat tasks.json
array_to_json -------------------------------------------------------------------------------[{"id":1,"type":"refill_cache","status":"new","params":"{\"threads\":-1}","output":"","exception":""},{"id":2,"type":"refill_cache","status":"new","params":"{\"threads\":-1}","output":"","exception":""}]
Полезная информация
● https://wiki.postgresql.org/wiki/Index_MaintenanceМонторинг индексов
● http://www.highload.ru/2013/abstracts/1170.htmlИндексы
● https://wiki.postgresql.org/wiki/Show_database_bloatTable and index bloat
● http://www.dalibo.org/_media/understanding_explain.pdfEXPLAIN
Recommended