Обновить

Комментарии 19

Пример плохой :) Это сколько же документов должно быть в базе чтобы партиционорование имело смысл :) Изначально партиционирование было придумано дла ОЧЕНЬ больших таблиц

Не могу судить насчет постгре но в оракле с партиционированием надо быть достаточно осторожным.

В постгре тоже надо быть очень осторожным.

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

Если doc_id до рефакторинга являлся первичным ключом, то его одного достаточно для связи вспомогательных таблиц с основной таблицей, и doc_id+doc_date не нужны. Разве нет?

После партиционирования возможна ситуация, когда в разных партициях записи могут содержать одинаковые значения doc_id, поэтому используется пара doc_id+doc_date (это также отражение требования «контроль возложить на СУБД»).

Вот всё-таки вопрос про модель данных. Есть исходная таблица.

create table docs_initial (
    doc_id integer not null,
    doc_date date not null,
    doc_number varchar(15) not null,
    doc_text text not null,
    constraint docs_initial_pk primary key (doc_id)
);

Её можно партицировать по-разному, но лучше партицировать так

create table doc (
    id integer not null,
    date date not null,
    number varchar(15) not null,
    text text not null,
    constraint doc_pk primary key (id)
) partition by range (id);

create index idx_doc__number on doc using btree (number);

create index idx_doc__date on doc using btree (date);

Тогда PK сохранится как есть и будет поддерживать глобальную уникальность.

Второй уникальный индекс по номеру документа, конечно, поддерживать уникальность глобально уже не сможет. Чтобы смог, нужна другая "проекция" этой таблицы, партицированная уже по doc_number.

create table doc_number (
    number varchar(15) not null
    constraint doc_number_pk primary key (number)
) partition by hash (number);

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

Именно при таком подходе обеспечен минимальный объем переделок в базе и приложениях.

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

Каким образом партиционирование по id поможет улучшить выполнение запросов, которые зависят от даты документа (фильтруют по ней)?

Синхронизация на триггерах имеет ряд недостатков. Из-за отсутствия FK, вероятно, не получится возложить контроль синхронности данных на СУБД.

Если id serial, то будет высокая прямая корреляция между значениями id и датой. Поэтому на каждой из партиций надо добавить через check её фактический диапазон дат, тогда предикат по дате будет отключать ненужные партиции при constraint_exclusion=on.

Допустим id не serial, а, например uuid. Зачем он вообще такой нужен? Он будет ничуть не лучше pk(number).

Корреляция может быть, а может и быть, поле типа serial определяет default, но не запрещает вставлять другие значения. При партиционировании по id может понадобиться прогнозировать соотношение дат и диапазонов id, устанавливать/корректировать диапазоны.

Прогнозировать соотношение дат и диапазонов id не нужно, поскольку все партиции будут одинаковы по числу строк и примерно равны по объёму. Это число строк можно выбрать таким, чтобы последняя горячая партиция гарантированно оставалась в shared buffers. Конечно же, это число может меняться со временем в зависимости от доступных ресурсов. Что же касается вставки других значений = это ведь при большом изменениях данных требуется, и это само изменение по времени должно случиться сильно позже, чем даты в добавляемых документах. Если так, то правильнее создавать под такую операцию отдельную партицию (или несколько) со своими границами.

Касательно второго утверждения про FK.

  1. В этой модели возможно создание FK

  2. Ошибочно думать, что FK надёжнее триггеров, поскольку сам FK реализован на основе триггеров

Hidden text
create table doc (
    id integer not null,
    date date not null,
    number varchar(15) not null,
    text text not null,
    constraint pk_doc primary key (id)
) partition by range (id);

create index idx_doc__number on doc using btree (number);
create index idx_doc__date on doc using btree (date);

create table doc_0_100000 partition of doc for values from (0) to (100000);
create table doc_100000_200000 partition of doc for values from (100000) to (200000);
create table doc_200000_300000 partition of doc for values from (200000) to (300000);
create table doc_300000_400000 partition of doc for values from (300000) to (400000);
create table doc_400000_500000 partition of doc for values from (400000) to (500000);
create table doc_500000_600000 partition of doc for values from (500000) to (600000);
create table doc_600000_700000 partition of doc for values from (600000) to (700000);
create table doc_700000_800000 partition of doc for values from (700000) to (800000);
create table doc_800000_800000 partition of doc for values from (800000) to (900000);
create table doc_900000_1000000 partition of doc for values from (900000) to (1000000);
create table doc_number (
    number varchar(15) not null,
    constraint pk_doc_number primary key (number)
) partition by hash (number);

create table doc_number_p0 partition of doc_number for values with (modulus 10, remainder 0);
create table doc_number_p1 partition of doc_number for values with (modulus 10, remainder 1);
create table doc_number_p2 partition of doc_number for values with (modulus 10, remainder 2);
create table doc_number_p3 partition of doc_number for values with (modulus 10, remainder 3);
create table doc_number_p4 partition of doc_number for values with (modulus 10, remainder 4);
create table doc_number_p5 partition of doc_number for values with (modulus 10, remainder 5);
create table doc_number_p6 partition of doc_number for values with (modulus 10, remainder 6);
create table doc_number_p7 partition of doc_number for values with (modulus 10, remainder 7);
create table doc_number_p8 partition of doc_number for values with (modulus 10, remainder 8);
create table doc_number_p9 partition of doc_number for values with (modulus 10, remainder 9);
alter table doc add constraint fk_doc__doc_number__number foreign key(number) references doc_number(number);
insert into doc_number(number) select number from doc;
alter table doc add constraint fk_doc__doc_number__number foreign key(number) references doc_number(number);

Добавим ещё партицию и данные в doc

create table doc_1000000_1100000 partition of doc for values from (1000000) to (1100000);

insert into doc(id, date, number, text) select id.id, '2022-02-24'::date + id.id * interval '1s', left(encode(decode(md5(id.id::text), 'hex'), 'base64'), 15), md5(hashint8(id.id)::text) from generate_series(1000000, 1099999, 1) as id(id);
ОШИБКА:  INSERT или UPDATE в таблице "doc_1000000_1100000" нарушает ограничение внешнего ключа "fk_doc__doc_number__number"

Выключим триггеры, повторим, отвалидируем

set session_replication_role = 'replica';
insert into doc(id, date, number, text) select id.id, '2022-02-24'::date + id.id * interval '1s', left(encode(decode(md5(id.id::text), 'hex'), 'base64'), 15), md5(hashint8(id.id)::text) from generate_series(1000000, 1099999, 1) as id(id);
--INSERT 0 100000
reset session_replication_role;
alter table doc validate constraint fk_doc__doc_number__number;
--ALTER TABLE

Итого, в примере выше получено неконсистентное состояние с живым FK за счёт выключения триггеров.

Любопытна политика OTUS: начинаешь серьёзное техническое обсуждение и сразу игнор. В вашем обучении такой же подход с игнором?

В указанной модели несколько документов могут ссылаться на одну запись во "второй проекции" таблице doc_number?

Могут, это должно быть решено организацией триггера. При добавлении в doc он сперва добавит строку в партицированную таблицу doc_number, а затем в doc.

Равнозначность триггеров и FK по надёжности я уже доказал в предыдущем сообщении, этот момент Вы не прокомментировали.

Дополнительно обозначу, что эту модель можно превратить из партицированной в шардированную, добавив в doc_number столбец doc_id и превратив doc_number в распределённый индекс по номеру документа.

После партиционирования возможна ситуация, когда в разных партициях записи могут содержать одинаковые значения doc_id, поэтому используется пара doc_id+doc_date

Получается, поле doc_date мутабельно? По номеру doc_number мы получаем связку id+date, по ней идем уже в нужную партицию, и при этом где-то там могут лежать "мусорные" записи с тем же id, но другой датой?

А партицирование по doc_number не выглядит эффективней? Если предположить, что doc_number (или какой-то иной счетчик) идут в нарастающей последовательности, и запрашиваются / редактируются обычно самые свежие документы, то это также бы решило проблему "локализации" доступа.

плюсы партицирования по сравнению с индексом

  1. гораздо лучше ускоряет низкоселективную выборку

  2. не замедляет инсерт

  3. не занимает место

  4. обмен и добавление партиций - супер эффективная вещь для масс-инсерта

Минусы:

  1. апдейт поля партицирования невозможен

  2. индексов много - партицировангие только одно

  3. для высокоселективных выборок - размер метаданных увеличивается критически

апдейт поля партицирования невозможен

Обновление полей по которым выполняется партиционирование работает, может быть не совсем ожидаемое поведение из-за реализации, если существует FK на PK партиционированной таблицы.

Хорошая статья о том, как делать не надо. Единственное, что не жалко было времени на чтение, чтобы пришлось подумать, почему так плохо сделали реорганизацию. Фраза "в разных партициях записи могут содержать одинаковые значения doc_id" - просто шедевральна. Обновлять дату тоже придется в двух таблицах одновременно, возможно перемещая записи из партиции в партицию? В ORACLE на эту тему есть глобальный индекс. Поэтому при поиске по ID ищется запись в одно индексе, а не в 50 маленьких кусочках. Надеюсь тут тоже такое потом появится (в MS SQL тоже, как в PG, нельзя сделать глобальный уникальный индекс поверх всех партиций). Также понравилась идея "Recheck Cond" в плане запроса. Кажется, что это гениально. PS: только изучаю Posgresql, что-то нравится, что-то вымораживает.

PS: в MS SQL на эту тему сделал кластеризованный ключ по PrDate+PrId (в итоге данные гарантированно посортированы по дате). А по PrId отдельный некластеризованный уникальный индекс. Благодаря этому большинство запросов (все они с фильтром за период) смотрят только таблицу, только нужные записи. Ходить через индекс по дате было бы менее гуманно. Большая часть запросов "за сегодня" - вообще смотрят только небольшой кусок данных. Плюс эта же дата попадает по все другие индексы, что тоже часто удобно, и обычно не мешает. Никаких партиций не потребовалось. По сути это получилось партиционирование по дню.

В PG, к сожалению, в настоящее время нет кластеризованного индекса, который постоянно поддерживал бы порядок записей.

Фраза "в разных партициях записи могут содержать одинаковые значения doc_id" - просто шедевральна.

При партиционировании по дате - да, технически могут и СУБД это допустит. "Прилететь" такие данные могут, отказываться от такого контроля не хотелось бы.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS