Настройка производительности PostgreSQL для ускорения выполнения запросов

by itisgood

Задача

Наша цель состоит в том, чтобы сделать выполнение фиктивного запроса в базе данных PostgreSQL быстрее, используя только встроенные инструменты, доступные в базе данных.

Операционная система и версии программного обеспечения

  • Операционная система: Red Hat Enterprise Linux 7.5
  • Программное обеспечение: сервер PostgreSQL 9.2

Требования

Установка и запуск сервера PostgreSQL. Доступ к инструменту командной строки psql

Введение

PostgreSQL – это надежная база данных с открытым исходным кодом, доступная во многих современных хранилищах дистрибутива.

Простота использования, возможность использования расширений и стабильность, которые он обеспечивает, все добавляют к его популярности. Предоставляя базовую функциональность, например, отвечая на запросы SQL, последовательно сохраняйте вставленные данные, обрабатывайте транзакции и т. д.

Большинство зрелых решений для баз данных предоставляют инструменты и ноу-хау по настройке базы данных, определению возможных узких мест и решению проблем производительности связанный с тем, что система, работающая от данного решения, растет.

PostgreSQL не является исключением, и в этом руководстве мы будем использовать встроенный инструмент, чтобы сделать медленный запрос завершенным быстрее.

Это далека от базы данных реального мира, но можно прибегнуть к использованию встроенных инструментов. Мы будем использовать сервер PostgreSQL версии 9.2 в Red Hat Linux 7.5, но инструменты, показанные в этом руководстве, представлены в более старых версиях базы данных и операционной системы.

Проблема, которая будет решена

Рассмотрим эту простую таблицу (имена столбцов не требуют пояснений):

foobardb=# \d+ employees
Table "public.employees"
Column | Type | Modifiers | Storage | Stats target | Description
------------------+---------+-----------------------------------------------------+----------+--------------+-------------
emp_id | numeric | not null default nextval('employees_seq'::regclass) | main | |
first_name | text | not null | extended | |
last_name | text | not null | extended | |
birth_year | numeric | not null | main | |
birth_month | numeric | not null | main | |
birth_dayofmonth | numeric | not null | main | |
Indexes:
"employees_pkey" PRIMARY KEY, btree (emp_id)
Has OIDs: no

С такими записями как:

foobardb=# select * from employees limit 2;
emp_id | first_name | last_name | birth_year | birth_month | birth_dayofmonth
--------+------------+-----------+------------+-------------+------------------
1 | Emily | James | 1983 | 3 | 20
2 | John | Smith | 1990 | 8 | 12

В этом примере мы являемся компанией  и развернули приложение под названием HBapp, которое отправляет письмо «Happy Birthday» сотруднику в день его рождения.

Приложение запрашивает базу данных каждое утро, чтобы найти получателей в течение дня (до работы, мы не хотим убивать нашу базу данных HR из доброты).

Приложение ищет следующий запрос для поиска получателей:

foobardb=# select emp_id, first_name, last_name from employees where birth_month = 3 and birth_dayofmonth = 20;

emp_id | first_name | last_name
--------+------------+-----------
1 | Emily | James

Все работает нормально, пользователи получают свою почту.

Многие другие приложения используют базу данных и таблицу сотрудников внутри, например, учет и BI.

Компания  растет, и поэтому растет штат сотрудников.

Со временем приложение работает слишком долго, а выполнение перекрывается с началом рабочего времени, что приводит к медленному времени отклика базы данных в критически важных приложениях.

Мы должны что-то сделать, чтобы этот запрос выполнялся быстрее, или приложение будет неработоспособным, и с ним в Nice Company будет меньше внимания.

В этом примере мы не будем использовать какие-либо дополнительные инструменты для решения проблемы, только один из них предоставляется базовой установкой. Посмотрим, как разработчик базы данных выполнит запрос с объяснением.

Мы не тестируем на производстве; мы создаем базу данных для тестирования, создаем таблицу и вставляем в нее двух сотрудников, упомянутых выше.

Мы используем те же самые значения для запроса в этом учебном пособии, поэтому при любом запуске только одна запись будет соответствовать запросу: Emily James.

Затем мы запускаем запрос с предыдущим объяснением, чтобы увидеть, как он выполняется с минимальными данными в таблице:

foobardb=# explain analyze select emp_id, first_name, last_name from employees where birth_month = 3 and birth_dayofmonth = 20;
QUERY PLAN
-----------------------------------------------------------------------------------------------------
Seq Scan on employees (cost=0.00..15.40 rows=1 width=96) (actual time=0.023..0.025 rows=1 loops=1)
Filter: ((birth_month = 3::numeric) AND (birth_dayofmonth = 20::numeric))
Rows Removed by Filter: 1
Total runtime: 0.076 ms
(4 rows)

Это очень быстро.

Возможно, так быстро, как только компания сначала развернула HBapp.

Давайте подражать состоянию текущего производства foobardb, загрузив в базу данных как можно больше (фальшивых) сотрудников в нашей базе данных (обратите внимание: нам потребуется тот же размер хранилища в тестовой базе данных, что и в производстве).

Мы просто используем bash для заполнения тестовой базы данных (если у нас есть 500 000 сотрудников в производстве):

$ for j in {1..500000} ; do echo "insert into employees (first_name, last_name, birth_year, birth_month, birth_dayofmonth) values ('user$j','Test',1900,01,01);"; done | psql -d foobardb

Сейчас у нас 500002 сотрудников:

foobardb=# select count(*) from employees;
count
--------
500002
(1 row)

Давайте снова запустим запрос explain:

foobardb=# explain analyze select emp_id, first_name, last_name from employees where birth_month = 3 and birth_dayofmonth = 20; QUERY PLAN ---------------------------------------------------------------------------------------------------------- Seq Scan on employees (cost=0.00..11667.63 rows=1 width=22) (actual time=0.012..150.998 rows=1 loops=1) Filter: ((birth_month = 3::numeric) AND (birth_dayofmonth = 20::numeric)) Rows Removed by Filter: 500001 Total runtime: 151.059 ms

У нас все еще есть только одно совпадение, но запрос значительно медленнее.

Мы должны заметить первый узел планировщика: Seq Scan, который означает последовательное сканирование – база данных считывает всю таблицу, а нам нужна только одна запись, например, grep в bash.

Фактически, это может быть на самом деле медленнее, чем grep.

Если мы экспортируем таблицу в файл csv с именем /tmp/exp500k.csv:

 foobardb=# copy employees to '/tmp/exp500k.csv' delimiter ',' CSV HEADER;
COPY 500002

И grepнем информацию, которая нам нужна (мы ищем 20-й день третьего месяца, последние два значения в файле csv в каждой строке):

$ time grep ",3,20" /tmp/exp500k.csv
1,Emily,James,1983,3,20

real 0m0.067s
user 0m0.018s
sys 0m0.010s

Решение является причиной индексации

. Ни один сотрудник не может иметь более одной даты рождения, которая состоит из ровно одного birth_year, birth_month и birth_dayofmonth – поэтому эти три поля предоставляют уникальное значение для этого конкретного пользователя.

И пользователь идентифицирует его / ее emp_id (в компании с таким же именем может быть более одного сотрудника).

Если мы объявим ограничение для этих четырех полей, будет создан неявный индекс:

foobardb=# alter table employees add constraint birth_uniq unique (emp_id, birth_year, birth_month, birth_dayofmonth);
NOTICE: ALTER TABLE / ADD UNIQUE will create implicit index "birth_uniq" for table "employees"

Итак, мы получили индекс для четырех полей, посмотрим, как работает наш запрос:

foobardb=# explain analyze select emp_id, first_name, last_name from employees where birth_month = 3 and birth_dayofmonth = 20;
QUERY PLAN
------------------------------------------------------------------------------------------------------------
Seq Scan on employees (cost=0.00..11667.19 rows=1 width=22) (actual time=103.131..151.084 rows=1 loops=1)
Filter: ((birth_month = 3::numeric) AND (birth_dayofmonth = 20::numeric))
Rows Removed by Filter: 500001
Total runtime: 151.103 ms
(4 rows)

Это идентично последнему, и мы можем видеть, что план одинаков, индекс не используется.

Давайте создадим еще один индекс с помощью уникального ограничения на emp_id, birth_month и birth_dayofmonth (в конце концов, мы не запрашиваем для birth_year в HBapp):

foobardb=# alter table employees add constraint birth_uniq_m_dom unique (emp_id, birth_month, birth_dayofmonth);
NOTICE: ALTER TABLE / ADD UNIQUE will create implicit index "birth_uniq_m_dom" for table "employees"

Давайте посмотрим на результат нашей настройки:

foobardb=# explain analyze select emp_id, first_name, last_name from employees where birth_month = 3 and birth_dayofmonth = 20;
QUERY PLAN
-----------------------------------------------------------------------------------------------------------
Seq Scan on employees (cost=0.00..11667.19 rows=1 width=22) (actual time=97.187..139.858 rows=1 loops=1)
Filter: ((birth_month = 3::numeric) AND (birth_dayofmonth = 20::numeric))
Rows Removed by Filter: 500001
Total runtime: 139.879 ms
(4 rows)

Ничего. Разница выше связана с использованием кешей, но план одинаков.

Пойдем дальше. Затем мы создадим еще один индекс для emp_id и birth_month:

foobardb=# alter table employees add constraint birth_uniq_m unique (emp_id, birth_month);
NOTICE: ALTER TABLE / ADD UNIQUE will create implicit index "birth_uniq_m" for table "employees"

И запустите запрос еще раз:

foobardb=# explain analyze select emp_id, first_name, last_name from employees where birth_month = 3 and birth_dayofmonth = 20;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------
Index Scan using birth_uniq_m on employees (cost=0.00..11464.19 rows=1 width=22) (actual time=0.089..95.605 rows=1 loops=1)
Index Cond: (birth_month = 3::numeric)
Filter: (birth_dayofmonth = 20::numeric)
Total runtime: 95.630 ms
(4 rows)

Успех! Запрос на 40% быстрее, и мы можем видеть, что план изменился: база данных больше не сканирует всю таблицу, но использует индекс на birth_month и emp_id.

Мы создали все миксы из четырех полей, осталось только одно. Стоит попробовать:

foobardb=# alter table employees add constraint birth_uniq_dom unique (emp_id, birth_dayofmonth);
NOTICE: ALTER TABLE / ADD UNIQUE will create implicit index "birth_uniq_dom" for table "employees"

Последний индекс создается на полях emp_id и birth_dayofmonth.

И результат:

foobardb=# explain analyze select emp_id, first_name, last_name from employees where birth_month = 3 and birth_dayofmonth = 20;
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------
Index Scan using birth_uniq_dom on employees (cost=0.00..11464.19 rows=1 width=22) (actual time=0.025..72.394 rows=1 loops=1)
Index Cond: (birth_dayofmonth = 20::numeric)
Filter: (birth_month = 3::numeric)
Total runtime: 72.421 ms
(4 rows)

Теперь наш запрос примерно на 49% быстрее, используя последний (и только последний) индекс.

Наша таблица и связанные с ней индексы выглядят следующим образом:

foobardb=# \d+ employees
Table "public.employees"
Column | Type | Modifiers | Storage | Stats target | Description
------------------+---------+-----------------------------------------------------+----------+--------------+-------------
emp_id | numeric | not null default nextval('employees_seq'::regclass) | main | |
first_name | text | not null | extended | |
last_name | text | not null | extended | |
birth_year | numeric | not null | main | |
birth_month | numeric | not null | main | |
birth_dayofmonth | numeric | not null | main | |
Indexes:
"employees_pkey" PRIMARY KEY, btree (emp_id)
"birth_uniq" UNIQUE CONSTRAINT, btree (emp_id, birth_year, birth_month, birth_dayofmonth)
"birth_uniq_dom" UNIQUE CONSTRAINT, btree (emp_id, birth_dayofmonth)
"birth_uniq_m" UNIQUE CONSTRAINT, btree (emp_id, birth_month)
"birth_uniq_m_dom" UNIQUE CONSTRAINT, btree (emp_id, birth_month, birth_dayofmonth)
Has OIDs: no

Нам не нужны промежуточные индексы, в плане четко указано, что они не будут их использовать, поэтому мы их дропаем:

foobardb=# alter table employees drop constraint birth_uniq;
ALTER TABLE
foobardb=# alter table employees drop constraint birth_uniq_m;
ALTER TABLE
foobardb=# alter table employees drop constraint birth_uniq_m_dom;
ALTER TABLE

В итоге наша таблица получает только один дополнительный индекс, который является низкой ценой для двойной скорости HBapp:

foobardb=# \d+ employees
Table "public.employees"
Column | Type | Modifiers | Storage | Stats target | Description
------------------+---------+-----------------------------------------------------+----------+--------------+-------------
emp_id | numeric | not null default nextval('employees_seq'::regclass) | main | |
first_name | text | not null | extended | |
last_name | text | not null | extended | |
birth_year | numeric | not null | main | |
birth_month | numeric | not null | main | |
birth_dayofmonth | numeric | not null | main | |
Indexes:
"employees_pkey" PRIMARY KEY, btree (emp_id)
"birth_uniq_dom" UNIQUE CONSTRAINT, btree (emp_id, birth_dayofmonth)
Has OIDs: no

И мы можем представить нашу настройку для производства, добавив индекс, который мы видели наиболее полезным:

alter table employees add constraint birth_uniq_dom unique (emp_id, birth_dayofmonth);

Вывод

Излишне говорить, что это всего лишь фиктивный пример.

Маловероятно, что вы будете хранить дату рождения вашего сотрудника в трех отдельных полях, в то время как вы можете использовать поле типа даты, что позволяет выполнять операции, связанные с датой, намного проще, чем сравнивать значения месяца и дня как целые числа. Также обратите внимание, что приведенные выше объяснения запросов не подходят для чрезмерного тестирования.

В сценарии реального мира вам необходимо проверить влияние нового объекта базы данных на любое другое приложение, использующее базу данных, а также компоненты вашей системы, которые взаимодействуют с HBapp.

Например, в этом случае, если мы сможем обрабатывать таблицу получателей в 50% от исходного времени ответа, мы можем фактически создать 200% писем на другом конце приложения (скажем, HBapp работает последовательно для все 500 дочерней компании Nice Company), что может привести к пиковой нагрузке где-то еще – возможно, почтовые серверы получат много писем «С днем ​​рождения» для ретрансляции, прежде чем они отправят ежедневные отчеты руководству, что приведет к задержкам доставки. Также немного далека от реальности, что кто-то настраивает базу данных, создаст индексы со слепой пробкой и ошибкой – или, по крайней мере, будем надеяться, что это так происходит в компании, использующей много людей.

Обратите внимание, однако, что мы получили 50% -ное повышение производительности по запросу, только используя встроенную функцию объяснения PostgreSQL, чтобы идентифицировать один индекс, который может быть полезен в данной ситуации. Мы также показали, что любая реляционная база данных не лучше, чем четкий текстовый поиск, если мы не используем их, поскольку они предназначены для использования.

 

You may also like

Leave a Comment