Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Объектно-реляционная СУБД PostgreSQL. Учебное пособие

.pdf
Скачиваний:
0
Добавлен:
12.08.2026
Размер:
787 Кб
Скачать

Filter:

((("Команда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

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]