5.11. Секционирование таблиц#
5.11. Секционирование таблиц #
Tantor SE-1C поддерживает базовое секционирование таблиц. В данном разделе описывается, зачем нужно секционирование и как его реализовать при проектировании базы данных.
5.11.1. Обзор #
Секционирование подразумевает разделение данных, логически относящихся к одной большой таблице, на более мелкие физические части. Секционирование имеет несколько преимуществ:
Производительность запросов может быть значительно улучшена в определенных ситуациях, особенно когда большая часть часто используемых строк таблицы находится в одной или в нескольких секциях. Секционирование эффективно заменяет верхние уровни дерева индексов, что повышает вероятность нахождения в памяти наиболее часто используемых частей индексов.
Когда запросы или операции обновления обращаются к большой части одной секции, производительность может быть улучшена путем использования последовательного сканирования этой секции вместо использования индекса, что требует произвольного чтения данных, разбросанных по всей таблице.
Массовые загрузки и удаления могут быть выполнены путем добавления или удаления секций, если соответствующий сценарий использования был предусмотрен при проектировании соответсвующих секций. Удаление отдельной секции с использованием команды
DROP TABLE
или выполнение командыALTER TABLE DETACH PARTITION
проходит гораздо быстрее, чем массовая операция удаления. Эти команды также полностью устраняют издержки операций очисткиVACUUM
, вызванной массовым удалением данных командойDELETE
.Редко используемые данные можно перенести на более дешевые и медленные носители данных.
Перечисленные возможности нужны когда таблицы очень большие. Размер таблицы, для которого может быть полезно секционирование, зависит от приложения, хотя общее правило гласит, что размер таблицы должен превышать физическую память сервера базы данных.
Tantor SE-1C предлагает встроенную поддержку следующих вариантов секционирования:
- Range Partitioning #
Таблица делится на “диапазоны”, определенные по ключевому столбцу или набору столбцов, без перекрытия диапазонов значений, назначенных разным секциям. Например, можно разделить данные по диапазонам дат или по диапазонам идентификаторов для конкретных бизнес-объектов. При этом в диапазон включается нижнее значение и исключается верхнее. Например, если диапазон одной секции - от
1
до10
, а диапазон следующей - от10
до20
, то значение10
принадлежит второй секции, а не первой.- List Partitioning #
Таблица делится путем явного перечисления значений ключа (-ей), относящихся к каждой секции.
- Hash Partitioning #
Таблица делится путем указания модуля и остатка для каждой секции. Каждая секция будет содержать строки, для которых хеш-значение ключа разделения, деленное на указанный модуль, даст указанный остаток.
Если для вашего приложения требуются другие формы секционирования, не указанные выше, можно использовать альтернативные методы, такие как наследование и представления UNION ALL
. Такие методы более гибкие, но они дают те же преимущества в производительности что и встроенное декларативное секционирование.
5.11.2. Декларативное разделение #
Tantor SE-1C позволяет объявить, что таблица разделена на секции. Таблица, которая разделена на секции, называется секционированной таблицей. Объявление состоит из метода разделения, описанного выше, а также списка столбцов или выражений, которые используются в качестве ключа разделения.
Сама секционированная таблица является “виртуальной” таблицей, не имеющей собственного хранилища. Хранилище используется секциям, которые являются обычными таблицами, связанными с секционированной таблицей. Каждая секция хранит подмножество данных, определенное границами секции. Все строки, вставленные в секционированную таблицу, будут направлены в соответствующую секцию на основе значений столбца(-ов) ключа разделения. Изменение ключа разделения строки приведет к перемещению ее в другую секцию, если она больше не удовлетворяет границам ее исходной секции.
Секции сами могут быть определены как секционированные таблицы, что приводит к разделению на подсекции. Хотя все секции должны иметь те же столбцы, что и их родительская секционированная таблица, секции могут иметь свои собственные индексы, ограничения и значения по умолчанию, отличные от других секций. См. CREATE TABLE для получения более подробной информации о создании секционированных таблиц и секций.
Невозможно из обычной таблицы сделать секционированную и наоборот. Однако, можно добавить существующую обычную или секционированную таблицу в качестве секции секционированной таблицы или удалить секцию из секционированной таблицы, преобразовав ее в самостоятельную таблицу. Это может упростить и ускорить многие процессы обслуживания. См. ALTER TABLE для получения дополнительной информации о подкомандах ATTACH PARTITION
и DETACH PARTITION
.
Секции могут также быть внешними таблицами, однако необходимо быть внимательным при их использовании, поскольку тогда пользователь сам несет ответственность за то, чтобы содержимое внешней таблицы удовлетворяло правилу секционирования. Есть и другие ограничения. См. CREATE FOREIGN TABLE для получения дополнительной информации.
5.11.2.1. Пример #
Предположим, что мы создаем базу данных для крупной компании по производству мороженого. Компания учитывает максимальные температуры и продажи мороженого в каждом регионе каждый день. По сути, мы хотим создать таблицу:
CREATE TABLE measurement ( city_id int not null, logdate date not null, peaktemp int, unitsales int );
Мы знаем, что большинство запросов будут обращаться только к данным за последнюю неделю, месяц или квартал, так как в основном таблицы будут использоваться при подготовке текущих отчетов для руководства. Чтобы уменьшить объем хранящихся старых данных, мы будем хранить только данные за последние 3 года. В начале каждого месяца мы будем удалять самые старые данные за месяц. В этом случае можно использовать секционирование, чтобы удовлетворить различные требования к таблице показателей.
Для использования декларативного секционирования в этом случае выполните следующие шаги:
Создайте таблицу
measurement
в виде секционированной таблицы, указав предложениеPARTITION BY
, которое включает метод секционирования (RANGE
в данном случае) и список столбцов, которые будут использоваться в качестве ключа разделения.CREATE TABLE measurement ( city_id int not null, logdate date not null, peaktemp int, unitsales int ) PARTITION BY RANGE (logdate);
Создайте секции. При определении каждой секции необходимо указывать границы, соответствующие методу разделения и ключу разделения родительской таблицы. Обратите внимание, что если при указании границ значения новой секции пересекаются со значениями в одной или нескольких существующих секциях, будет сгенерирована ошибка.
Созданные таким образом секции являются обычными таблицами (или, возможно, внешними таблицами) Tantor SE-1C. Можно указать отдельное табличное пространство и параметры хранения для каждой секции.
В нашем примере каждая секция должна содержать данные за один месяц, чтобы выполнялось требование удаления данных по месяцам за одну операцию. Таким образом, команды могут выглядеть следующим образом:
CREATE TABLE measurement_y2006m02 PARTITION OF measurement FOR VALUES FROM ('2006-02-01') TO ('2006-03-01'); CREATE TABLE measurement_y2006m03 PARTITION OF measurement FOR VALUES FROM ('2006-03-01') TO ('2006-04-01'); ... CREATE TABLE measurement_y2007m11 PARTITION OF measurement FOR VALUES FROM ('2007-11-01') TO ('2007-12-01'); CREATE TABLE measurement_y2007m12 PARTITION OF measurement FOR VALUES FROM ('2007-12-01') TO ('2008-01-01') TABLESPACE fasttablespace; CREATE TABLE measurement_y2008m01 PARTITION OF measurement FOR VALUES FROM ('2008-01-01') TO ('2008-02-01') WITH (parallel_workers = 4) TABLESPACE fasttablespace;
(Помните, что соседние секции могут иметь общее значение границы, так как верхние границы не включаются в диапазон).
Если нужно реализовать разделение на подсекции, снова укажите
PARTITION BY
в командах, используемых для создания отдельных секций, например:CREATE TABLE measurement_y2006m02 PARTITION OF measurement FOR VALUES FROM ('2006-02-01') TO ('2006-03-01') PARTITION BY RANGE (peaktemp);
После создания секций
measurement_y2006m02
, любые данные, вставленные вmeasurement
и отображаемые наmeasurement_y2006m02
(или данные, которые непосредственно вставлены вmeasurement_y2006m02
, что допускается при условии удовлетворения соответствующего ограничения секции), будут дополнительно перенаправлены в одну из секций на основе столбцаpeaktemp
. Указанный ключ разделения может пересекаться с ключом разделения родительского объекта, хотя следует быть внимательным при указании границ подсекции, чтобы набор данных, который она принимает, входил в подмножество, определенное границами самой секции; система не будет проверять это проверять.Вставка данных в родительскую таблицу, которая не соответствует одной из существующих секций, вызовет ошибку; соответствующая секция должна быть добавлена вручную.
Нет необходимости вручную создавать ограничения таблицы, задающие границы секций. Такие ограничения будут созданы автоматически.
Создайте индекс на ключевом столбце(-ах), а также любые другие необходимые индексы, на секционированной таблице. (Индекс ключа не является строго обязательным, но в большинстве сценариев он полезен). Это автоматически создает соответствующий индекс на каждой секции, и любые секции, которые вы создадите или присоедините позже, также будут иметь такой индекс. Индексы или ограничения уникальности, объявленные на секционированной таблице, являются “виртуальным” так же, как и сама секционированная таблица: фактически данные находятся в дочерних индексах на отдельных секционированных таблицах.
CREATE INDEX ON measurement (logdate);
Убедитесь, что параметр конфигурации enable_partition_pruning не отключен в файле
postgresql.conf
. Если он отключен, запросы не будут оптимизированы так, как требуется.
В приведенном выше примере нужно создавать новую секцию каждый месяц, поэтому было бы полезно написать скрипт, который автоматически генерирует необходимый DDL-код.
5.11.2.2. Обслуживание секций #
Обычно не предполагается, что набор секций, установленных при первоначальном определении таблицы, будет оставаться неизменным. Часто требуется удалить секции, содержащие старые данные, и периодически добавлять новые секции для новых данных. Одним из самых важных преимуществ секционирования является возможность выполнения этой трудоемкой задачи практически мгновенно путем манипулирования структурой секций, а не физического перемещения больших объемов данных.
Самый простой способ удаления старых данных — удалить секцию, которая больше не нужна:
DROP TABLE measurement_y2006m02;
Это может очень быстро удалить миллионы записей, потому что не требуется отдельно удалять каждую запись. Однако обратите внимание, что вышеуказанная команда требует установления блокировки на родительской таблице ACCESS EXCLUSIVE
.
Еще один способ, который часто предпочтительнее, — это удалить секцию из секционированной таблицы, но сохранить доступ к ней как к отдельной таблице. Возможны два варианта:
ALTER TABLE measurement DETACH PARTITION measurement_y2006m02; ALTER TABLE measurement DETACH PARTITION measurement_y2006m02 CONCURRENTLY;
Это позволяет выполнять дополнительные операции с данными перед их удалением. Например, можно создать резервные копии данных с помощью команды COPY
, приложения pg_dump или аналогичных инструментов. Также можно агрегировать данные в более компактные форматы, выполнить прочие манипуляции с данными или сгенерировать отчеты. Первая форма команды требует блокировку на родительской таблице ACCESS EXCLUSIVE
. Добавление квалификатора CONCURRENTLY
во втором варианте позволит выполнить отсоединение с использованием только блокировки SHARE UPDATE EXCLUSIVE
на родительской таблице, но рекомендуем ознакомиться с ALTER TABLE ... DETACH PARTITION
для получения подробной информации об ограничениях.
Также можно добавить новую секцию для обработки новых данных. Мы можем создать пустую секцию в секционированной таблице, выполнив те же шаги, что и при создании секций изначально:
CREATE TABLE measurement_y2008m02 PARTITION OF measurement FOR VALUES FROM ('2008-02-01') TO ('2008-03-01') TABLESPACE fasttablespace;
В качестве альтернативы, иногда более удобно создать
новую таблицу вне структуры секций, и присоединить ее как
секцию позже. Это позволяет загружать новые данные, проверять их и
преобразовывать до того, как они появятся в секционированной таблице.
Более того, операция ATTACH PARTITION
требует
только блокировку SHARE UPDATE EXCLUSIVE
на
секционированной таблице, в отличие от блокировки ACCESS
EXCLUSIVE
, которая требуется командой CREATE TABLE
... PARTITION OF
, поэтому она более совместима с параллельными
операциями на секционированной таблице.
Опция CREATE TABLE ... LIKE
позволяет избежать трудоемкого повторного определения родительской таблицы:
CREATE TABLE measurement_y2008m02 (LIKE measurement INCLUDING DEFAULTS INCLUDING CONSTRAINTS) TABLESPACE fasttablespace; ALTER TABLE measurement_y2008m02 ADD CONSTRAINT y2008m02 CHECK ( logdate >= DATE '2008-02-01' AND logdate < DATE '2008-03-01' ); \copy measurement_y2008m02 from 'measurement_y2008m02' -- possibly some other data preparation work ALTER TABLE measurement ATTACH PARTITION measurement_y2008m02 FOR VALUES FROM ('2008-02-01') TO ('2008-03-01' );
Перед выполнением команды ATTACH PARTITION
рекомендуется создать ограничение CHECK
на таблице, которая будет присоединена, соответствующее ожидаемому ограничению секции, как показано выше. Таким образом, система сможет пропустить сканирование, которое в противном случае потребуется для проверки неявного ограничения секции. Без ограничения CHECK
таблица будет сканироваться для проверки ограничения секции при удержании блокировки ACCESS EXCLUSIVE
на этой секции. Рекомендуется удалить избыточное ограничение CHECK
после завершения команды ATTACH PARTITION
. Если присоединяемая таблица сама является секционированной таблицей, то каждая из ее подсекций будет рекурсивно блокироваться и сканироваться до тех пор, пока не будет найдено подходящее ограничение CHECK
или не будут достигнуты конечные секции.
Аналогично, если у секционированной таблицы есть секция DEFAULT
,
рекомендуется создать ограничение CHECK
,
которое исключает ограничение секции, которая будет присоединяться. Если
этого не сделать, то секция DEFAULT
будет
просканирована, чтобы подтвердить, что в ней нет записей, которые должны находиться в
присоединяемой секции. Эта операция будет выполнена при удержании блокировки ACCESS EXCLUSIVE
на секции
DEFAULT
. Если секция DEFAULT
сама является секционированной таблицей, то каждая из ее секций будет
рекурсивно проверяться так же как и присоединяемая таблица, как
упоминалось выше.
Как уже объяснялось выше, на секционированных таблицах можно создать индексы таким образом, чтобы они автоматически применялись ко всей иерархии. Это очень удобно, поскольку индексирование будет применяться не только к существующим секциям, но и к любым секциям, создаваемым в будущем. Однако есть ограничение: нельзя использовать квалификатор CONCURRENTLY
при создании такого секционированного индекса. Чтобы избежать длительных блокировок, можно использовать команду CREATE INDEX ON ONLY
для секционированной таблицы; такой индекс помечается как недействительный, и не используется автоматически на секциях. Индексы на секциях можно создавать отдельно с использованием CONCURRENTLY
, а затем присоединять их к индексу на родительской таблице с помощью команды ALTER INDEX .. ATTACH PARTITION
. После присоединения индексов всех секций к родительскому, родительский индекс автоматически помечается как действительный. Пример:
CREATE INDEX measurement_usls_idx ON ONLY measurement (unitsales); CREATE INDEX CONCURRENTLY measurement_usls_200602_idx ON measurement_y2006m02 (unitsales); ALTER INDEX measurement_usls_idx ATTACH PARTITION measurement_usls_200602_idx; ...
Этот вариант можно использовать с ограничениями UNIQUE
и
PRIMARY KEY
тоже; индексы создаются
неявно при создании ограничения. Пример:
ALTER TABLE ONLY measurement ADD UNIQUE (city_id, logdate); ALTER TABLE measurement_y2006m02 ADD UNIQUE (city_id, logdate); ALTER INDEX measurement_city_id_logdate_key ATTACH PARTITION measurement_y2006m02_city_id_logdate_key; ...
5.11.2.3. Ограничения #
Следующие ограничения применяются к секционированным таблицам:
Для создания ограничения уникальности или первичного ключа на секционированной таблице, ключи разделения не должны содержать выражений или вызовов функций, и ограничение охватывает все столбцы ключа разделения. Это ограничение существует, потому что отдельные индексы, составляющие ограничение, могут непосредственно обеспечивать уникальность только в своих собственных секциях; поэтому сама структура секционирования должна гарантировать отсутствие дубликатов в разных секциях.
Нет способа создать ограничение-исключение, охватывающее всю секционированную таблицу. Возможно только установить такое ограничение на каждую конечную секцию отдельно. Это также обусловлено невозможностью применения ограничений между секциями.
Триггеры
BEFORE ROW
наINSERT
не могут изменить, в какую секцию будет помещена новая строка.Смешивание временных и постоянных отношений в одном дереве секционирования не допустимо. Следовательно, если секционированная таблица является постоянной, то таковыми должны быть и ее секции, и то же правило действует для временных секционированных таблиц. При использовании временных отношений все члены дерева секционирования должны быть из одной сессии.
Отдельные секции связаны с своей секционированной таблицей посредством наследования. Однако невозможно использовать общие функции наследования с декларативно секционированными таблицами или их секциями, как обсуждается ниже. В частности, секция не может иметь других родителей, кроме секционированной таблицы, секцией которой она является, и таблица не может наследовать одновременно от секционированной таблицы, и от обычной таблицы. Это означает, что секционированные таблицы и их секции никогда не имеют общей иерархии наследования с обычными таблицами.
Поскольку иерархия секционирования, состоящая из секционированной таблицы и ее секций, также является иерархией наследования, tableoid
и все обычные правила наследования применяются, как описано в Раздел 5.10, но с несколькими исключениями:
В секции не может быть столбцов, которых нет в родительской таблице. Невозможно определить такие столбцы при создании секций с помощью
CREATE TABLE
, также невозможно добавить столбцы к секциям черезALTER TABLE
. Таблицы могут быть добавлены как секции с помощьюALTER TABLE ... ATTACH PARTITION
только если их столбцы полностью совпадают о столбцами родительской таблицы.Оба ограничения
CHECK
иNOT NULL
наследуются всеми секциями секционированной таблицы. ОграниченияCHECK
, помеченные какNO INHERIT
, не могут быть созданы на секционированных таблицах. Вы не можете удалить ограничениеNOT NULL
на столбце секции, если такое же ограничение присутствует в родительской таблице.Использование
ONLY
для добавления или удаления ограничения только на секционированную таблицу поддерживается, пока нет секций. Если секции уже существуют, использованиеONLY
приведет к ошибке для любых ограничений, кромеUNIQUE
иPRIMARY KEY
. Но можно добавлять и удалять ограничения на самих секциях (если они отсутствуют в родительской таблице).Поскольку у секционированной таблицы нет собственных данных, попытки использовать команду
TRUNCATE
ONLY
для секционированной таблицы всегда будут вызывать ошибку.
5.11.3. Секционирование с использованием наследования #
В то время как встроенное декларативное секционирование подходит для большинства стандартных сценариев использования, есть некоторые обстоятельства, когда нужен более гибкий подход. Секционирование может быть реализовано с использованием наследования таблицы, что позволяет использовать несколько функций, не поддерживаемых декларативным секционированием, таких как:
Для декларативного секционирования, секции должны иметь точно такой же набор столбцов, как и секционированная таблица, в то время как при наследовании таблицы, дочерние таблицы могут иметь дополнительные столбцы, отсутствующие в родительской.
Механизм наследования позволяет использовать множественное наследование.
Декларативное секционирование поддерживает только секционирование по диапазону, списку и хешу, в то время как наследование таблиц позволяет разделить данные по выбору пользователя. (Однако следует отметить, что если ограничение-исключение не может эффективно устранить дочерние таблицы из запросов, производительность запросов может быть низкой).
5.11.3.1. Пример #
В этом примере создается структура секционирования, эквивалентная структуре из примера декларативного секционирования выше. Используйте следующие шаги:
Создайте “корневую” таблицу, от которой будут наследовать все “дочерние” таблицы. Эта таблица не будет содержать данных. Не определяйте проверочных ограничений для этой таблицы, если вы не хотите, чтобы они применялись ко всем дочерним таблицам. Также нет смысла определять индексы или ограничения уникальности. В нашем примере корневая таблица — это таблица
measurement
, как она была изначально определена:CREATE TABLE measurement ( city_id int not null, logdate date not null, peaktemp int, unitsales int );
Создайте несколько дочерних таблиц, каждая из которых наследует от корневой таблицы. Обычно эти таблицы не добавляют никаких столбцов к набору, унаследованному от корневой таблицы. Как и в случае с декларативным секционированием, эти таблицы являются обычными таблицами (или внешними таблицами) Tantor SE-1C во всех отношениях.
CREATE TABLE measurement_y2006m02 () INHERITS (measurement); CREATE TABLE measurement_y2006m03 () INHERITS (measurement); ... CREATE TABLE measurement_y2007m11 () INHERITS (measurement); CREATE TABLE measurement_y2007m12 () INHERITS (measurement); CREATE TABLE measurement_y2008m01 () INHERITS (measurement);
Добавьте неперекрывающиеся ограничения таблицы к дочерним таблицам, чтобы определить допустимые значения ключей в каждой таблице.
Типичными примерами могут быть:
CHECK ( x = 1 ) CHECK ( county IN ( 'Oxfordshire', 'Buckinghamshire', 'Warwickshire' )) CHECK ( outletID >= 100 AND outletID < 200 )
Убедитесь, что ограничения гарантируют,что значения ключа не относятся сразу к нескольким дочерним таблицам. При установке диапазонных ограничений часто допускается следующая ошибка:
CHECK ( outletID BETWEEN 100 AND 200 ) CHECK ( outletID BETWEEN 200 AND 300 )
Ошибка вызвана тем, что непонятно к какой дочерней таблице принадлежит значение 200. Диапазоны должны быть определены следующим образом:
CREATE TABLE measurement_y2006m02 ( CHECK ( logdate >= DATE '2006-02-01' AND logdate < DATE '2006-03-01' ) ) INHERITS (measurement); CREATE TABLE measurement_y2006m03 ( CHECK ( logdate >= DATE '2006-03-01' AND logdate < DATE '2006-04-01' ) ) INHERITS (measurement); ... CREATE TABLE measurement_y2007m11 ( CHECK ( logdate >= DATE '2007-11-01' AND logdate < DATE '2007-12-01' ) ) INHERITS (measurement); CREATE TABLE measurement_y2007m12 ( CHECK ( logdate >= DATE '2007-12-01' AND logdate < DATE '2008-01-01' ) ) INHERITS (measurement); CREATE TABLE measurement_y2008m01 ( CHECK ( logdate >= DATE '2008-01-01' AND logdate < DATE '2008-02-01' ) ) INHERITS (measurement);
Для каждой дочерней таблицы создайте индекс на ключевом столбце (-ах), а также любые другие необходимые индексы.
CREATE INDEX measurement_y2006m02_logdate ON measurement_y2006m02 (logdate); CREATE INDEX measurement_y2006m03_logdate ON measurement_y2006m03 (logdate); CREATE INDEX measurement_y2007m11_logdate ON measurement_y2007m11 (logdate); CREATE INDEX measurement_y2007m12_logdate ON measurement_y2007m12 (logdate); CREATE INDEX measurement_y2008m01_logdate ON measurement_y2008m01 (logdate);
Нам нужно, чтобы приложение могло выполнить
INSERT INTO measurement ...
и данные направлялись в соответствующую дочернюю таблицу. Можно добиться этого, добавив соответствующую триггерную функцию к корневой таблице. Если данные будут добавляться только в последнюю дочернюю таблицу, можно использовать очень простую триггерную функцию:CREATE OR REPLACE FUNCTION measurement_insert_trigger() RETURNS TRIGGER AS $$ BEGIN INSERT INTO measurement_y2008m01 VALUES (NEW.*); RETURN NULL; END; $$ LANGUAGE plpgsql;
После создания функции мы создаем триггер, который вызывает триггерную функцию:
CREATE TRIGGER insert_measurement_trigger BEFORE INSERT ON measurement FOR EACH ROW EXECUTE FUNCTION measurement_insert_trigger();
Необходимо переопределять триггерную функцию каждый месяц, чтобы она всегда вставляла данные в текущую дочернюю таблицу. Однако, определение триггера не требуется обновлять.
Может понадобиться вставить данные и позволить серверу автоматически определять дочернюю таблицу, в которую должна быть добавлена строка. Это можно сделать с помощью более сложной триггерной функции, например:
CREATE OR REPLACE FUNCTION measurement_insert_trigger() RETURNS TRIGGER AS $$ BEGIN IF ( NEW.logdate >= DATE '2006-02-01' AND NEW.logdate < DATE '2006-03-01' ) THEN INSERT INTO measurement_y2006m02 VALUES (NEW.*); ELSIF ( NEW.logdate >= DATE '2006-03-01' AND NEW.logdate < DATE '2006-04-01' ) THEN INSERT INTO measurement_y2006m03 VALUES (NEW.*); ... ELSIF ( NEW.logdate >= DATE '2008-01-01' AND NEW.logdate < DATE '2008-02-01' ) THEN INSERT INTO measurement_y2008m01 VALUES (NEW.*); ELSE RAISE EXCEPTION 'Date out of range. Fix the measurement_insert_trigger() function!'; END IF; RETURN NULL; END; $$ LANGUAGE plpgsql;
Определение триггера остается прежним. Обратите внимание, что каждое условие
IF
должно точно соответствовать ограничениюCHECK
для соответствующей дочерней таблицы.Хотя эта функция сложнее, чем в случае с одним месяцем, ее не надо часто обновлять, так как ветки можно добавить заранее.
Примечание
На практике, возможно, лучше проверить сначала самую последнюю дочернюю таблицу, если большинство вставок происходит в эту таблицу. Для простоты мы показали проверки триггера в том же порядке, что и в других фрагментах кода в примере.
Другой подход к перенаправлению вставок в соответствующую дочернюю таблицу — настройка не триггера, а правил на корневой таблице. Например:
CREATE RULE measurement_insert_y2006m02 AS ON INSERT TO measurement WHERE ( logdate >= DATE '2006-02-01' AND logdate < DATE '2006-03-01' ) DO INSTEAD INSERT INTO measurement_y2006m02 VALUES (NEW.*); ... CREATE RULE measurement_insert_y2008m01 AS ON INSERT TO measurement WHERE ( logdate >= DATE '2008-01-01' AND logdate < DATE '2008-02-01' ) DO INSTEAD INSERT INTO measurement_y2008m01 VALUES (NEW.*);
Настройка правил влечет за собой больше издержек, чем настройка триггер, но эти издержки относятся ко всему запросу, а не к каждой строке, поэтому этот метод может быть оптимальным для массовой вставки данных. Однако в большинстве случаев метод с использованием триггера будет обеспечивать лучшую производительность.
Обратите внимание, что команда
COPY
игнорирует правила. Если нужно использоватьCOPY
для вставки данных, нужно будет скопировать их в правильную дочернюю таблицу, а не напрямую в корневую.COPY
не отменяет триггеры, поэтому вы можете использовать его обычным способом, если используете триггеры.Еще одним недостатком подхода с использованием правил является невозможность сгенерировать ошибку, если набор правил не охватывает вставку данных; данные будут без предупреждения помещены в корневую таблицу.
Убедитесь, что параметр конфигурации constraint_exclusion не отключен в файле
postgresql.conf
; в противном случае, дочерние таблицы могут сканироваться, даже когда это не требуется.
Как мы видим, сложная иерархия таблиц может потребовать написания большого объема DDL-кода. В приведенном выше примере мы создаем новую дочернюю таблицу каждый месяц, поэтому было бы разумно написать скрипт, который автоматически генерирует необходимый DDL-код.
5.11.3.2. Обслуживание таблиц, секционированных посредством наследования #
Чтобы быстро удалить старые данные, просто удалите дочернюю таблицу, которая больше не нужна:
DROP TABLE measurement_y2006m02;
Чтобы удалить дочернюю таблицу из иерархии наследования, но сохранить доступ к ней как к отдельной таблице, выполните:
ALTER TABLE measurement_y2006m02 NO INHERIT measurement;
Чтобы добавить новую дочернюю таблицу для обработки новых данных, создайте пустую дочернюю таблицу так же, как были созданы исходные дочерние таблицы выше:
CREATE TABLE measurement_y2008m02 ( CHECK ( logdate >= DATE '2008-02-01' AND logdate < DATE '2008-03-01' ) ) INHERITS (measurement);
В качестве альтернативы, можно создать и заполнить новую дочернюю таблицу перед добавлением ее в иерархию таблиц. Это может позволить загружать, проверять и преобразовывать данные перед их отображением в запросах к родительской таблице.
CREATE TABLE measurement_y2008m02 (LIKE measurement INCLUDING DEFAULTS INCLUDING CONSTRAINTS); ALTER TABLE measurement_y2008m02 ADD CONSTRAINT y2008m02 CHECK ( logdate >= DATE '2008-02-01' AND logdate < DATE '2008-03-01' ); \copy measurement_y2008m02 from 'measurement_y2008m02' -- possibly some other data preparation work ALTER TABLE measurement_y2008m02 INHERIT measurement;
5.11.3.3. Ограничения в использовании #
Пояснения к секционированию, реализованному посредством наследования:
Нет способа автоматически проверить, что все ограничения
CHECK
взаимоисключающие. Безопаснее создавать код, который генерирует дочерние таблицы и создает и/или изменяет связанные объекты, чем делать это вручную.Индексы и ограничения внешних ключей применяются только к отдельным таблицам, а не к их дочерним таблицам, поэтому они имеют некоторые особенности, о которых следует знать.
Схемы, показанные здесь, предполагают, что значения ключевого столбца(-ов) строки никогда не изменяются, или по крайней мере не изменяются достаточно, чтобы требовать перемещения в другую секцию.
UPDATE
, пытающийся сделать это, завершится неудачей из-за ограниченийCHECK
. Если вам нужно обрабатывать такие случаи, вы можете установить соответствующие триггеры обновления на дочерние таблицы, но это усложняет управление структурой.Если вы используете команды
VACUUM
илиANALYZE
вручную, не забудьте, что их нужно запускать отдельно для каждой дочерней таблицы. Такая команда:ANALYZE measurement;
будет обрабатывать только корневую таблицу.
Операторы
INSERT
, содержащие предложенияON CONFLICT
, вряд ли будут работать так, как ожидается, поскольку действиеON CONFLICT
выполняется только в случае уникальных нарушений в указанном целевом отношении, а не на его дочерних отношениях.Для направления строк в желаемую дочернюю таблицу потребуются триггеры или правила, если приложение явно не знает о схеме секционирования. Написание триггеров может быть сложным и они будут гораздо медленнее, чем внутреннее распределение кортежей, выполняемое декларативным секционированием.
5.11.4. Отсечение секций #
Отсечение секций — это техника оптимизации запросов, которая улучшает производительность для таблиц с декларативным секционированием. В качестве примера:
SET enable_partition_pruning = on; -- the default SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01';
Без отсечения секций, указанный запрос будет сканировать каждую из секций таблицы measurement
. При включенном отсечении секций, планировщик будет анализировать определение каждой секции и подтверждать, что секцию не нужно сканировать, потому что она не может содержать строки, удовлетворяющие предложению WHERE
запроса. Когда планировщик может это доказать, он исключает (отсекает) секцию из плана запроса.
С помощью команды EXPLAIN и параметра конфигурации enable_partition_pruning можно показать разницу между планом, для которого было выполнено отсечение секций, и планом, для которого оно не было выполнено. Типичный неоптимизированный план для такой таблицы выглядит следующим образом:
SET enable_partition_pruning = off; EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; QUERY PLAN ----------------------------------------------------------------------------------- Aggregate (cost=188.76..188.77 rows=1 width=8) -> Append (cost=0.00..181.05 rows=3085 width=0) -> Seq Scan on measurement_y2006m02 (cost=0.00..33.12 rows=617 width=0) Filter: (logdate >= '2008-01-01'::date) -> Seq Scan on measurement_y2006m03 (cost=0.00..33.12 rows=617 width=0) Filter: (logdate >= '2008-01-01'::date) ... -> Seq Scan on measurement_y2007m11 (cost=0.00..33.12 rows=617 width=0) Filter: (logdate >= '2008-01-01'::date) -> Seq Scan on measurement_y2007m12 (cost=0.00..33.12 rows=617 width=0) Filter: (logdate >= '2008-01-01'::date) -> Seq Scan on measurement_y2008m01 (cost=0.00..33.12 rows=617 width=0) Filter: (logdate >= '2008-01-01'::date)
Некоторые или все секции могут использовать индексные сканирования вместо последовательного сканирования всей таблицы, но здесь главное — это то, что нет необходимости сканировать старые секции для ответа на этот запрос. Когда мы включаем отсечение секций, мы получаем значительно более дешевый план, который даст тот же ответ:
SET enable_partition_pruning = on; EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; QUERY PLAN ----------------------------------------------------------------------------------- Aggregate (cost=37.75..37.76 rows=1 width=8) -> Seq Scan on measurement_y2008m01 (cost=0.00..33.12 rows=617 width=0) Filter: (logdate >= '2008-01-01'::date)
Обратите внимание, что отсечение секций осуществляется только на основе ограничений, определенных неявно ключами секций, а не наличием индексов. Поэтому нет необходимости определять индексы на ключевых столбцах. Необходимость создания индекса для определенной секции зависит от того, ожидается ли, что запросы, сканирующие секцию, в основном будут сканировать большую часть секции или только небольшую часть. Индекс будет полезен в последнем случае, но не в первом.
Выполнение отсечения секций может осуществляться не только во время планирования
конкретного запроса, но и во время его выполнения. Это полезно, так как отсекается больше секций, в случаях когда предложения содержат выражения,
значения которых неизвестны на этапе планирования запроса, например,
параметры, определенные в операторе PREPARE
, значения, получаемые из подзапросов, или параметризованные значения во внутренней стороне соединения с вложенным циклом. Отсечение секций во время выполнения
может возможно в любом из следующих временных интервалов:
Во время инициализации плана запроса. Здесь может выполняться отсечение секций для значений параметров, которые известны на этапе инициализации выполнения. Секции, которые были отсечены на этом этапе, не будут отображаться в выводе запроса
EXPLAIN
илиEXPLAIN ANALYZE
. Возможно определить количество секций, которые были удалены на этом этапе, посмотрев свойство “Subplans Removed” в выводеEXPLAIN
.Во время фактического выполнения плана запроса также может выполняться отсечение секций для удаления секций, использующих значения, которые становятся известны только во время выполнения запроса. Это включает значения из подзапросов и значения из параметров времени выполнения, таких как параметры из параметризованных вложенных соединений циклов. Поскольку значение этих параметров может изменяться много раз во время выполнения запроса, отсечение секций выполняется каждый раз, когда изменяется один из используемых параметров выполнения отсечения. Чтобы определить, были ли отсечены секции на этом этапе, необходимо внимательно изучить свойства
loops
в выводеEXPLAIN ANALYZE
. Подзапросы, соответствующие разным секциям, могут иметь разные значения для этого свойства в зависимости от того, сколько раз каждая из них была отсечена во время выполнения. Некоторые могут быть показаны как(never executed)
(никогда не выполнялись), если они отсекались каждый раз.
Отсечение секций может быть выполнено с помощью параметра enable_partition_pruning.
5.11.5. Секционирование и исключение ограничений #
Исключение по ограничению - это метод оптимизации запросов, аналогичный отсечению секций. В основном он используется для секционирования, реализованного с помощью устаревшего метода наследования, но может использоваться и для других целей, включая декларативное секционирование.
Исключение по ограничению работает очень похоже на отсечение секций, за исключением того, что оно использует ограничения CHECK
каждой таблицы — отсюда и название — в то время как отсечение секций использует границы секций таблицы, которые существуют только в случае декларативного секционирования. Еще одно отличие заключается в том, что исключение по ограничению применяется только на этапе планирования; не предпринимается попытка удалить секции на этапе выполнения.
То, что исключение по ограничению использует ограничения CHECK
,
что делает его медленнее по сравнению с отсечением секции, иногда может
быть преимуществом: поскольку ограничения могут быть определены
даже для таблиц с декларативным секционированием, в дополнение к их внутренним
границам секций, исключение по ограничению может
убрать дополнительные секции из плана запроса.
По умолчанию (и это рекомендуемое поведение) значение constraint_exclusion не является ни on
, ни off
, а является промежуточным значением, называемым partition
, что приводит к применению его только к запросам, которые, вероятно, работают с унаследованными секционированными таблицами. Значение on
заставляет планировщик проверять CHECK
-ограничения во всех запросах, даже в простых, которые, вероятно, не принесут особой пользы.
Следующие пояснения относятся к исключению по ограничению:
Исключение по ограничению применяется только во время планирования запроса, в отличие от отсечения секций, которое также может применяться во время выполнения запроса.
Исключение по ограничению работает только тогда, когда в предложении
WHERE
запроса содержатся константы (или внешне предоставленные параметры). Например, сравнение с непостоянной функцией, такой какCURRENT_TIMESTAMP
, не может быть оптимизировано, поскольку планировщик не может знать, в какую дочернюю таблицу может попасть значение функции во время выполнения.Ограничения секционирования должны быть простыми, иначе планировщик не сумеет подтвердить, что дочерние таблицы, возможно, не нужно обрабатывать. Используйте простые условия равенства для секционирования по списку или простые тесты диапазона для секционирования по диапазону, как показано в предыдущих примерах. Лучше если ограничения секционирования содержат только сравнения столбцов секционирования с константами, используя операторы, которые могут быть использованы с B-деревом, потому что только столбцы, которые могут быть использованы с B-деревом, разрешены в ключе разделения.
Все ограничения на все дочерние таблицы родительской таблицы проверяются при анализе исключения по ограничению, поэтому большое количество дочерних таблиц может значительно увеличить время планирования запросов. Как следствие устаревшее секционирование на основе наследования будет работать хорошо с не больше сотней дочерних таблиц; не пытайтесь использовать множество тысяч дочерних таблиц.
5.11.6. Лучшие практики декларативного разделения #
Способ секционирования таблицы следует выбирать очень внимательно, так как производительность планирования и выполнения запросов может быть пониженной из-за непродуманного проекта.
Одним из самых важных проектных решений будет выбор столбца или столбцов, по которым будет производиться секционирование данных. Часто более оптимальным выбором будет секционирование по столбцу или набору столбцов, которые наиболее часто встречаются в предложениях WHERE
запросов, выполняемых на секционированной таблице. Предложения WHERE
, совместимые с ограничениями границ секций, могут использоваться для отсечения ненужных секций. Однако требования к PRIMARY KEY
или ограничению UNIQUE
могут вынудить вас принять другие решения. При планировании стратегии секционирования также следует учитывать метод удаления ненужных данных. Целую секцию можно отсоединить достаточно быстро, поэтому может быть полезно разработать стратегию секционирования таким образом, чтобы все данные, которые нужно удалить сразу, находились в одной секции.
Выбор количества секций, на которое должна быть разделена таблица, также является важным решением. Недостаточное количество секций может означать, что индексы остаются слишком большими, а локальность данных остается низкой, что может привести к низким показателям попадания в кеш. Однако, слишком большое количество секций также может вызвать проблемы. Когда секций много, можно столкнуться с более длительным временем планирования запросов и более высоким потреблением памяти как во время планирования, так и во время выполнения запросов, как описано далее.
При выборе способа секционирования таблицы также важно учесть возможные изменения в будущем. Например, если вы выбираете одну секцию на каждого клиента и в настоящее время у вас небольшое количество крупных клиентов, учтите возможные последствия, если через несколько лет у вас вместо этого окажется большое количество маленьких клиентов. В этом случае можно выбрать секционирование по HASH
и выбрать разумное количество секций, а не пытаться разделить данные по списку (LIST
) и надеяться, что количество клиентов не превысит практический предел секционирования данных.
Разделение на подсекции может быть полезным для дальнейшего разделения секций, которые по расчетам станут больше, чем другие секции. Другой вариант — использовать секционирование по диапазонам с несколькими столбцами в ключе разделения. Любой из этих вариантов может привести к избыточному количеству секций, поэтому не следует переусердствовать.
Важно учитывать издержки на секционирование при планировании и выполнении запросов. Планировщик запросов обычно хорошо справляется с иерархиями секций, содержащими до нескольких тысяч секций, при условии, что типичные запросы позволяют планировщику запросов отсеять все, кроме небольшого количества секций. Время планирования увеличивается, а потребление памяти становится выше, когда после отсеивания секций планировщиком остается больше секций. Также стоит учитывать, что при большом количестве секций потребление памяти сервера может значительно увеличиваться со временем, особенно если множество сессий работает с большим количеством секций. Это происходит потому, что каждая секция требует загрузки своих метаданных в локальную память каждой сессии, который с ним взаимодействует.
С хранилищами данных может иметь смысл использовать большее количество секций, чем с типом рабочей нагрузки OLTP. В общем, в хранилищах данных время планирования запросов менее важно, так как большая часть времени тратится на выполнение запроса. С любым из этих двух типов рабочей нагрузки важно принимать правильные решения заранее, так как повторное секционирование больших объемов данных может быть очень медленным. Для оптимизации стратегии секционирования рекомендуется эмулировать предполагаемую рабочую нагрузку. Не стоит считать, что больше секций лучше, чем меньше, и наоборот.