Объектно-реляционная СУБД PostgreSQL. Учебное пособие
.pdfFilter:
((("Команда1" = 'Бразилия'::text) AND ("Забитых1" > "Забитых2")) OR (("Команда2" = 'Бразилия'::text) AND ("Забитых2" > "Забитых1")))
Rows Removed by Filter: 46 Total runtime: 0.121 ms
Посмотрим, что добавилось в выдаче:
actual time – реальное время в миллисекундах, затраченное для получения первой строки и всех строк соответственно;
rows – реальное количество строк, полученных при Seq Scan; loops – сколько раз пришлось выполнить операцию Seq Scan;
Rows Removed by Filter – число строк, «удалённых» при применении фильтра;
Total runtime – общее время выполнения запроса.
Рассмотрим ещё один пример. Пусть БД «Торговля» содержит таблицу Товары (КодТовара, Марка, КодПоставщика, ЕдиницаИзмерения, Цена) и Поставщики (КодПоставщика, Название, Страна). Задача: для каждой страны определить количество поставщиков. Выполним сразу EXPLAIN ANALYZE:
EXPLAIN ANALYZE
SELECT Поставщики.Страна, count(*) FROM Поставщики
GROUP BY Поставщики.Страна;
Получим
HashAggregate (cost=11.50..12.50 rows=100 width=58) (actual time=0.079..0.092 rows=17 loops=1)
-> Seq Scan on "Поставщики" (cost=0.00..11.00 rows=100 width=58) (actual time=0.011..0.024 rows=29 loops=1)
Total runtime: 0.170 ms
Мы видим, что выполняется последовательный просмотр (Seq Scan) таблицы Поставщики, количество поставщиков подсчитывается методом
111
HashAggregate, который «собирает» записи с одинаковым значением хэш-функции для поля Страна.
Добавим сортировку:
EXPLAIN ANALYZE
SELECT Поставщики.Страна, count(*) FROM Поставщики
GROUP BY Поставщики.Страна
ORDER BY Поставщики.Страна;
Получим
GroupAggregate (cost=14.32..16.07 rows=100 width=58) (actual time=0.247..0.291 rows=17 loops=1)
-> Sort (cost=14.32..14.57 rows=100 width=58) (actual time=0.231..0.244 rows=29 loops=1)
Sort Key: "Страна"
Sort Method: quicksort Memory: 18kB
-> Seq Scan on "Поставщики" (cost=0.00..11.00 rows=100 width=58) (actual time=0.009..0.029 rows=29 loops=1)
Total runtime: 0.352 ms
Теперь при последовательном просмотре записи упорядочиваются по ключу «Страна» методом быстрой сортировки. После этого для каждой группы записей с одним и тем же значением поля Страна (а после сортировки эти записи идут подряд!) применяется групповая агрегатная функция (GroupAggregate).
Следующий пример. Создадим таблицу Bigtable c ключевым полем id и вещественным числом двойной точности number:
CREATE TABLE Bigtable ( id serial,
number double precision,
CONSTRAINT pk_bigtable PRIMARY KEY (id)
);
Вставим в таблицу 100 000 случайных вещественных чисел от 0 до 1.
112
INSERT INTO Bigtable(number)
SELECT random() FROM generate_series(1, 100000);
Создадим индекс по полю number
CREATE INDEX idx_number ON Bigtable(number);
и выполним запрос
EXPLAIN SELECT * FROM Bigtable WHERE number>0.56 AND number<0.61;
Изучим план запроса
Bitmap Heap Scan on Bigtable (cost=113.21..731.66 rows=5163 width=12) Recheck Cond: ((number > 0.56::double precision)
AND (number < 0.61::double precision)) -> Bitmap Index Scan on idx_number
(cost=0.00..111.92 rows=5163 width=0) Index Cond: ((number > 0.56::double precision)
AND (number < 0.61::double precision))
Bitmap Index Scan использует созданный индекс для поиска записей, удовлетворяющих заданному условию (строка Index Cond (условие)). Для экономии памяти сканирование возвращает битовую карту полученного набора записей (т.е., бит 1 – для записей, удовлетворяющих заданному условию, и бит 0 – для остальных записей). Далее эту битовую карту использует метод Bitmap Heap Scan, который извлекает нужные записи из таблицы (при этом перепроверяется заданное условие – Recheck Cond). Заметим, что выборочное извлечение записей из таблицы «стоит» дороже, чем последовательный просмотр, потому выигрыш достигается только при небольшом количестве извлекаемых записей.
Изменим предыдущий запрос:
EXPLAIN SELECT * FROM Bigtable WHERE number>0.06 AND number<0.91;
тогда план запроса изменится (будет использован последовательный просмотр всех записей и отбор по фильтру):
113
Seq Scan on Bigtable (cost=0.00..2041.00 rows=84966 width=12) Filter: ((number > 0.06::double precision)
AND (number < 0.91::double precision))
Если отбор записей идёт по индексированному и неиндексированному столбцам, то используется комбинация индекса и фильтра. Выполним запрос:
EXPLAIN SELECT * FROM Bigtable
WHERE id % 2 = 0 AND number>0.56 AND number<0.61;
План запроса
Bitmap Heap Scan on Bigtable (cost=111.93..756.19 rows=26 width=12) Recheck Cond: ((number > 0.56::double precision)
AND (number < 0.61::double precision)) Filter: ((id % 2) = 0)
-> Bitmap Index Scan on idx_number
(cost=0.00..111.92 rows=5163 width=0) Index Cond: ((number > 0.56::double precision)
AND (number < 0.61::double precision))
В случае, если на каждое поле в запросе WHERE создан индекс (а также в случае сложного запроса над индексированным полем), планировщик может использовать команду BitmapAnd и/или BitmapOr. Пример. Выполним запрос:
EXPLAIN SELECT * FROM Bigtable WHERE number>0.06 AND number<0.16
OR number>0.56 AND number<0.61;
Получим
Bitmap Heap Scan on Bigtable (cost=339.35..1190.89 rows=14992 width=12) Recheck Cond: (((number > 0.06::double precision)
AND (number < 0.16::double precision)) OR ((number > 0.56::double precision) AND (number < 0.61::double precision)))
-> BitmapOr (cost=339.35..339.35 rows=15527 width=0) -> Bitmap Index Scan on idx_number
114
(cost=0.00..219.93 rows=10364 width=0) Index Cond: ((number > 0.06::double precision)
AND (number < 0.16::double precision)) -> Bitmap Index Scan on idx_number
(cost=0.00..111.92 rows=5163 width=0) Index Cond: ((number > 0.56::double precision)
AND (number < 0.61::double precision))
До сих пор мы рассматривали выборку из одной таблицы. Перейдем к запросам с соединением таблиц. Пусть есть две таблицы:
Отделы (Номер, НазваниеОтдела) и Сотрудники (ТабНомер, ФИО, Должность, НомерОтдела)
Подсчитаем число сотрудников в каждом отделе и рассмотрим план запроса:
EXPLAIN SELECT Отделы.Номер,
НазваниеОтдела, count(*) As ЧислоСотрудников FROM Отделы, Сотрудники
WHERE Отделы.Номер = Сотрудники.НомерОтдела GROUP BY Отделы.Номер;
HashAggregate (cost=69.81..77.51 rows=770 width=36) -> Hash Join (cost=37.67..65.96 rows=770 width=36)
Hash Cond: ("Сотрудники"."НомерОтдела" = "Отделы"."Номер") -> Seq Scan on "Сотрудники" (cost=0.00..17.70 rows=770 width=4) -> Hash (cost=22.30..22.30 rows=1230 width=36)
-> Seq Scan on "Отделы" (cost=0.00..22.30 rows=1230 width=36)
Для соединения таблиц используется метод Hash Join. На первом шаге просматривается таблица Отделы, при этом в памяти формируется таблица значений хэш-функции для поля Номер. На втором шаге просматривается таблица Сотрудники, для каждой её записи вычисляется хэш-значение поля НомерОтдела, и ищется в полученной на первом шаге таблице значений. При их совпадении вычисляется агрегатная функция.
115
Кроме Hash Join PosgreSQL может использовать Merge Join (слияние таблиц по полю, упорядоченному в каждой из таблиц) и Nested Join (использование вложенных запросов, аналог вложенных циклов в программировании).
6.2. Секционирование
Секционирование (или партиционирование – от английского partition – «часть, раздел») – это разделение больших таблиц базы данных на меньшие части. Секционирование таблицы позволяет базе данных при выполнении запроса просматривать не всю базу данных, а те несколько разделов, в которых содержится нужная информация. Такой подход даёт значительный выигрыш в целом ряде случаев, в частности, если большинство запросов относится к последним по времени записям таблицы.
СУБД PostgreSQL поддерживает два критерия для создания разделов:
А) Секционирование по диапазону значений. В этом варианте таблица разбивается на диапазоны значений некоторого поля или набора полей, причём диапазоны значений, отнесенных к различным разделам, не перекрываются.
Б) Секционирование по списку значений. В этом варианте таблица разбивается по фиксированным наборам ключевых значений для каждого раздела.
Алгоритм секционирования таблицы:
1)создается мастер-таблица, от которой будут наследоваться все остальные таблицы. Мастер-таблица не содержит данных, и по существу представляет собой шаблон, который определяет структуру каждой таблицы, предназначенной для хранения одного раздела;
2)создается несколько дочерних таблиц, структура которых наследуется от мастер-таблицы;
3)в дочерние таблицы добавляются условия разделения (например, по диапазону значений: CHECK (например, ID BETWEEN 100 AND 199);
4)для каждой дочерней таблицы создаётся индекс по ключевому полю, при необходимости можно также создавать и другие индексы;
116
5) для перенаправления добавляемых данных в соответствующую дочернюю таблицу как правило создаётся триггер для мастер-таблицы.
Замечание. При секционировании таблицы необходимо, чтобы параметр constraint_exclusion в конфигурационном файле postgresql.conf был включен, в противном случае запросы не будут учитывать наличие таблиц-разделов.
Приведём пример. Пусть мы хотим достаточно долго хранить в таблице информацию о запросах пользователя к нашей базе данных и ежемесячно на основе этой информации создавать отчет. Данные в такой таблице накапливаются быстро, создание отчетов требует большего времени, также как и удаление старых записей. Тут нам на помощь и приходит секционирование.
Итак, создадим мастер-таблицу:
CREATE TABLE my_logs ( id SERIAL PRIMARY KEY, user_id INT NOT NULL,
logdate TIMESTAMP NOT NULL, queue TEXT
) ;
Поскольку нам нужны отчеты каждый месяц, мы будем создавать дополнительные таблицы для каждого месяца. Для примера создадим две дочерние таблицы:
CREATE TABLE my_logs2016m10 (
CHECK ( logdate >= DATE ’2016-10-01’ AND logdate < DATE ’2016-11-01’)
) INHERITS (my_logs);
CREATE TABLE my_logs2016m11 (
CHECK ( logdate >= DATE ’2016-11-01’ AND logdate < DATE ’2016-12-01’)
) INHERITS (my_logs);
Таблицы my_logs2016m10 и my_logs2016m11 копируют структуру мастер-таблицы за исключением индексов. В этих таблицах с помощью CHECK задаётся диапазон значений, которые будут попадать в этот раздел. Поскольку
117
для секционирования мы используем поле logdate, создадим индекс на это поле на всех разделах:
CREATE INDEX idx_2016m10 ON my_logs2016m10 (logdate); CREATE INDEX idx_2016m11 ON my_logs2016m11 (logdate);
Далее создадим триггерную функцию, которая будет перенаправлять новые данные в соответствующую дочернюю таблицу.
CREATE OR REPLACE FUNCTION insert_date( ) RETURNS TRIGGER AS $$
BEGIN
IF ( NEW.logdate >= DATE ’2016-10-01’
AND NEW.logdate < DATE ’2016-11-01’ ) THEN INSERT INTO my_logs2016m10 VALUES (NEW.*);
ELSIF ( NEW.logdate >= DATE ’2016-11-01’
AND NEW.logdate < DATE ’2016-12-01’ ) THEN INSERT INTO my_logs2016m11 VALUES (NEW.*);
ELSE
RAISE EXCEPTION ’Дата вне имеющихся диапазонов’; END IF ;
RETURN NULL; END;
$$ LANGUAGE plpgsql;
Теперь осталось создать триггер на мастер-таблицу:
CREATE TRIGGER insert_trigger BEFORE INSERT ON my_logs
FOR EACH ROW EXECUTE PROCEDURE insert_date( ) ;
Секционирование настроено. Выполним теперь несколько запросов на вставку данных в таблицу my_logs:
INSERT INTO my_logs ( user_id, logdate, queue ) VALUES( 1, ’2016-10-30’, ’select data1…’); INSERT INTO my_logs ( user_id, logdate, queue )
VALUES( 2, ’2016-11-10’, ’select data2 …’) ; INSERT INTO my_logs ( user_id, logdate, queue )
VALUES( 1, ’2016-11-15’ , ’select data3 …’) ;
118
Теперь проверим, где хранятся введённые данные:
SELECT * FROM ONLY my_logs; – запрос не возвращает ни одной записи, так как выборка идёт из пустой мастер-таблицы (из-за ключевого слова ONLY)
SELECT * FROM my_logs2016m10; – запрос возвращает одну запись; SELECT * FROM my_logs2016m11; – запрос возвращает две записи; SELECT * FROM my_logs; – запрос возвращает все три записи.
Отсюда вывод: запросы, которые не ограничиваются одним или двумя разделами, пишутся как самые обычные запросы, например:
SELECT * FROM my_logs WHERE user_id = 1;
Если же мы точно знаем, где находятся нужные данные, то можно обращаться к конкретному разделу.
Обычно при использовании технологии секционирования некоторые разделы перестают получать данные и остаются неизменными. Это дает огромное преимущество при работе с данными, разделёнными на части. Вернёмся к нашему примеру. Если нам понадобится удалить данные за октябрь 2016 года, достаточно будет выполнить запрос
DROP TABLE my_logs2016m10;
Поскольку DROP TABLE работает гораздо быстрее, это лучше, чем удаление тысяч записей через DELETE. Можно также удалить раздел оператором ALTER TABLE. В этом случае данные раздела остаются в СУБД, но к ним не будет доступа с помощью запросов к мастер-таблице (заметим, что доступ через дочернюю таблицу сохраняется):
ALTER TABLE my_logs2016m10 NO INHERIT my_logs ;
Это удобно, если мы хотим впоследствии перенести эти данные в другое хранилище.
6.3. Масштабирование базы данных
Масштабирование баз данных – наиболее сложная задача, которую приходится решать, если при разработке базы данных предполагается
119
значительное увеличении объёма данных при эксплуатации БД. В основе масштабирования данных лежит принцип разделения данных на группы с выделением их на отдельные сервера. Существует две основные стратегии масштабирования – репликация и шардинг.
Репликация. При репликации создается полный дубликат базы данных на нескольких серверах. Чаще всего используют схему master – slave («хозяин – раб»):
Master – это основной сервер БД, куда первоначально поступают все данные. Также и все изменения в данных должны происходить только на этом сервере.
Slave – это вспомогательный сервер БД, на который копируются все данные с сервера master. Slave предназначен только для чтения данных. При большом количестве запросов к БД можно использовать несколько slave-серверов.
Обычно операций выборки данных намного больше, чем операций изменения данных, поэтому, репликация позволяет существенно снизить нагрузку на основной сервер за счет переноса операций чтения на slave-сервер.
К недостаткам репликации можно отнести рассогласование данных, которое возникает в случае задержки процесса копирования информации на slave-сервер. Зато это отличное средство для обеспечения физической целостности информации, хранящейся в базе данных. При выходе из строя мастер-сервера его всегда можно заменить slave-сервером. Чаще всего репликация используется именно из соображений надежности.
Также репликацию master-slave можно использовать для создания резервной копии базы данных. Резервная копия создаётся на slave-сервере, который работает в пассивном режиме. Все запросы, в том числе и запросы на выборку данных, обрабатываются на master-сервере, а slave-сервер только получает изменения данных, сделанные на master-сервере. Но, как и в предыдущем случае, при выходе из строя master-сервера выполнение всех операций перелаётся slave-серверу.
Шардинг. Шардинг – это другая техника масштабирования при работе с большими данными. Суть его в разделении базы данных на отдельные части так, чтобы каждую из них можно было вынести на отдельный сервер. При этом
120
