Комментарии 36
А если система должна быть доступна 24/7, то такие процессы начинают заметно ухудшать SLA
Вот тут я бы порекомендовал дать определение, что вы, как автор, подразумеваете под SLA. В терминах ITIL под статью подходят одна-две метрики из перечня SLI (service level indicator): Availability (доступность) и какая-нибудь связанная с быстродействием системы
Согласен. Имелась в виду именно доступность системы. Это уже профдеформация, когда клиент под SLA тоже подразумевает, в первую очередь, доступность системы.
С клиентами, иногда, тоже очень тяжело в плане непонимания, для чего ему нужны "лишние" девятки в availability. Часто (но не всегда), в корпоративном мире запланированный простой DWH с чётко ограниченными временными рамками воспринимается не так негативно, как неконтролируемая деградация быстродействия и увеличение время отклика на запросы пользователя / систем потребителей данных DHW. Организационные меры могут быть не менее эффективные, чем внутренняя оптимизация самого хранилища
Один из вариантов решения - это свалить данную проблему на клиента.
Не зная деталей вашей задачи, хочется понять. А почему для этого варианта не подходит view? Он ничего не стоит, создается за мгновение. Какие-то нюансы переименования во время открытых курсоров?
-------------------------------------------------
Ну и также, допустим есть таблица с некими данными. Я так понял, они нечто вроде аналитической выборки, т.е. инкрементальное добавление тут не подходит, надо именно пересчитать и создать новые данные (вроде объема продаж за последние 7 дней). Можете добавить колонку "актуальность". Таблиц может быть много, на идею это не влияет
Клиенты работают только с данными, где актуальность == "true" и у них все хорошо. Вы рассчитываете и льете новые даные с актуальность == "false" и никому не мешаете. Ну стала табличка в два раза больше, переживем. Потом в рамках одной транзакции меняете актуальность у двух наборов. Кроме люто длинной транзакции - проблем нет.
Но если данных много, а их у вас много, то проблема "длинной" транзакции обходится тем, что вы храните актуальность не как двухпозиционный переключатель, а как например дату. И тогда вы пишете новые данные с новой датой, а клиентам предоставляете данные через view, в коде которой спрятано обращение только к данных с актуальной датой. И тогда все что вам надо, это после заливки новых данных поменять актуальную дату. И нет длинной транзакции
Меня просто тригернула фраза "свалить проблему на клиента", так как использование view как интерфейса для прямого доступа к объектам БД является хорошей практикой, и позволяет незаметно от глаз клиента делать "под капотом" любой ахалай-махалай
А почему для этого варианта не подходит view?
Вы уже уходите в частности, явно выходящие за рамки статьи, интересуясь причинами возникновения таких таблиц.
Например, в одном из случаев, это детализация временных серий используемых для обучения моделей прогнозирования, позволяющая оценить достоверность прогноза. Только их подготовка (трансформация, валидации, фильтрации, сглаживания) занимает больше часа на 32-х ядрах и 256 ГБ оперативки. Какая тут на фиг VIEW? Результат - около терабайта. И есть целый ряд ограничений, не позволяющих получить выгоду при инкрементальном обновлении таблицы.
в рамках одной транзакции меняете актуальность у двух наборов
Можно, но по производительности обновление такой таблицы будет очень болезненно. Например, в таблице выше сейчас 2,627,101,700 записей. Сколько времени будет выполняться UPDATE на 5 миллиардах записей? Намного быстрее тогда просто сделать TRUNCATE, удалить индексы, залить данные из промежуточной таблицы и построить индексы заново.
это после заливки новых данных поменять актуальную дату
Не намного легче. Сколько времени будет выполняться INSERT и DELETE 2.5 миллиардов записей с обновлением всех индексов? И это не считая десятков гигабайт довеска на лишнее поле в самой таблице и в её индексах.
Вы вообще сами пытались заливать миллиарды записей в индексированную таблицу? В неиндексированную нежурналируемую (UNLOGGED) таблицу я почти без накладных расходов могу лить данные из десятков потоков. В индексированную - уже все намного более грустно.
View == интерфейс/API. Ви можете делать как вам оптимальнее в реальных таблицах, хоть с переименованием. Но для клиента намного проще спрятать потроха решения за ним. Оно само по себе ничего не стоит, и ничего не добавляет к накладным расходам. Это ж по сути, просто сохраненный запрос. И если нет нюансов с его пересозданием во время активніх запросов и опцией переименования - это явно опция, которая спрячет все детали от read only клиентов
Про как оптимальнее - ну я ж не вижу реальных таблиц. Тут вам карты в руки. Но здесь вопрос того, что вам критичнее, downtime или затраченное процессорное время. Если у вас выделенный сервер, за который вы платите всегда и он от этой нагрузки не уйдет в "философские размышления о вечном" - тогда лишняя процессорная нагрузка не стоит ничего. И ее можно брать по максимум
Но так как вы вынесли SLA в заголовок, разумно рассмотреть trade-off, при котором улучшение SLA будет оплачено менее ценным для вас ресурсом.
P.S. В любом случае єто ваша задача, и бенефиты от нашей дискуссии в практическую плоскость трансформировать можете только вы. Я - максимум узнать о вашем опыте
View == интерфейс/API. Ви можете делать как вам оптимальнее в реальных таблицах, хоть с переименованием. Но для клиента намного проще спрятать потроха решения за ним.
Если Вы ведете речь о пересоздании представления на другую таблицу, то это намного более трудоемкое решение, чем переименование таблиц, так как потребует или дублирования кода, или перехода на динамический SQL во всех процедурах, формирующих эти таблицы.
Ну, да. Но это уже вопрос как лучше переименовать :) Насколько я помню, view созданные как create ... as select * from <одна таблица>, не имеет ограничений по DML-операциям.
Соответственно, если у вас сейчас есть таблица DATA_1, то вам надо:
код формирования оставить как есть
назвать view как DATA_1, и код формирования тоже работает с view
в начале работы пересоздать view на набор на сегодня
клиентам выдать новый набор
Я понимаю, изменение интерфейса - это не очень приятно. Но я бы раз пошел на это изменение, получив клиентов, которые работают с view (это офигенно ценно само по себе), и после этого у вас развязаны руки
По сути, архитектурно вы можете прийти к ситуации, когда есть модель данных клиента, которые постороены на view. И тогда вы отвязываете модули наполнения от клиентов.
Я бы как архитектор, пошел на это решение чисто из-за его перспектив разделения на слои. Но вам виднее ваши накладные расходы/бюджет
P.S. Я просто обычно наличие слоя view для тех, кто лазит в базу закладываю сразу. Потом, через время, это становится мега удобно.
Еще раз, если пересоздавать VIEW с таблицы SomeTable1 на SomeTable2 и обратно, то все процедуры, участвующие в формировании этих таблиц должны быть переведены на динамический SQL. Ведь они через раз должны будут заполнять то SomeTable1, то SomeTable2. Поэтому переименовывать таблицы - намного меньшие трудозатраты. А результат тот же.
Я просто обычно наличие слоя view для тех, кто лазит в базу закладываю сразу.
Я наоборот этого избегаю. Потому что не раз уже разгребал у клиентов нагромождения VIEW в запросах друг на друге с такой глубиной, что планировщик запросов уже не мог разобраться, как оптимально выполнить запрос, уходя в глухие циклы.
Чтобы не быть голословным, предлагаю Вам самому сравнить планы запросов. Замените только имя таблицы car, на имя таблицы имеющейся у Вас в схеме public. Получаем список полей таблицы по VIEW information_schema.columns:
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'car'
AND table_schema = 'public';
и на прямую по таблицe pg_catalog.pg_attribute
SELECT attname
FROM pg_catalog.pg_attribute
WHERE attrelid = 'public.car'::regclass
AND attnum > 0
AND NOT attisdropped;
Результат один и тот же. А вот оверхед в первом запросе - грандиозный.
Еще раз, если пересоздавать VIEW с таблицы SomeTable1 на SomeTable2 и обратно, то все процедуры, участвующие в формировании этих таблиц должны быть переведены на динамический SQL. Ведь они через раз должны будут заполнять то SomeTable1, то SomeTable2. Поэтому переименовывать таблицы - намного меньшие трудозатраты. А результат тот же.
Не, идея в том, что скрипты всегда пишут во view. В начале натравил их на нужный набор и вперед. Поэтому код тоже остается один и тот же. Возможно в Postgre есть какие-то ограничения для тривиальных view, но я по памяти не помню такого
------------------------------
Про оверхед не понял. Сорян, под рукой сейчас экземпляра нет, куда-то в клауд ломиться ща не могу. Но предположу, что там view тянет инфу с кучи таблиц, и ясное дело, оптимизатор честно выполняет запрос из view, на который сверху накручен еще один критерий от тебя.
Туда ж не кладется бизнес логика, ну разве что довольно таки тривиальная и понятная. И точно без повторного использования, так как слоев должно быть именно два, а не макароны. Я даже стараюсь join в таком слое использовать по минимуму, чтобы клиент работал с логической моделью, а не с уже "типа" подготовленной единственной таблицей.
----------------------------
По код - есть еще опыт, когда стейджинговые процедуры сразу пишутся как параметрические и просто создаются по набор таблиц. Условно в коде вместо имени таблицы ее псевдоним, который при ее создании заменяется на то, который нужен здесь и сейчас.
В любом случае, спасибо за ваш опыт. Прикольно было почитать
скрипты всегда пишут во view
Во-первых, Вы что опять предлагаете заливать миллиарды записей в индексированную таблицу? Я же писал выше:
Вы вообще сами пытались заливать миллиарды записей в индексированную таблицу? В неиндексированную нежурналируемую (UNLOGGED) таблицу я почти без накладных расходов могу лить данные из десятков потоков. В индексированную - уже все намного более грустно.
Во-вторых, Вы точно о PostgreSQL пишете? В несколько потоков в PostgreSQL можно писать исключительно через CREATE TABLE ... AS или SELECT ... INTO, что по любому исключает запись в VIEW. https://www.postgresql.org/docs/current/when-can-parallel-query-be-used.html
Вы уж простите, но в один поток мне формировать вышеописанную таблицу не час, как на 32-х ядрах, а сутки, что уже ни в какие ворота не лезет.
По код - есть еще опыт, когда стейджинговые процедуры сразу пишутся как параметрические и просто создаются по набор таблиц.
Я сам люблю динамический SQL, но при этом отдаю себе отчет, что он требует больше времени для разработки и тяжелей в поддержке.
Что касается подмены имен схем для разных контуров, то я для этого использую при деплое C препроцессор (cpp). Благодаря чему могу иметь в одной базе множество контуров разработки в своих схемах по имени ветки в GIT. Ну и заодно макроопределения порой очень повышают читабельность кода и упрощают его написание.
что вам критичнее, downtime или затраченное процессорное время
Все критично. Но вариант с переименованием таблиц ничуть не уступает в downtime варианту пересоздания VIEW. Все так же упираемся в продолжительность транзакций клиентов. А вот остальные издержки - явно выше.
Еще возможен вариант с партициями с постоянно возрастающим значением. Пишем в новую партицию одной транзакцией (или перекидываем из временной таблицы, если СУБД так умеет). А клиент при работе с таблице определяет последнюю партицию и читает только ее. Устаревшие партиции грохать.
Это намного более трудоемко, чем просто переименовать таблицу. А преимуществ не даёт никаких.
Не так уж и трудоемко, несколько строчек кода.
Преимущество - можно не ждать читателей, пока они завершат текущую сессию чтения.
Но я не настаиваю, задачи бывают разные.
Не так уж и трудоемко, несколько строчек кода.
Нет. Это у меня несколько строчек кода. А в Вашем случае - переход на динамический SQL в процедурах, формирующих таблицу.
можно не ждать читателей, пока они завершат текущую сессию чтения
Плохо читали документацию. Если DETACH PARTITION еще допускает CONCURRENTLY, то ATTACH требует монопольного доступа к секционируемой таблице.
А так как формируются вообще то не одна таблица, а целый ряд связанных друг с другом, то без логической блокировки тут все равно не обойтись.
Кстати, а что скажет клиент, если ему посреди чтения миллиарда записей переименуют читаемую таблицу?
Сам не установил или снял блокировку - сам виноват. В статье указана необходимость использования логической блокировки клиентом.
Указана, но уже больно кратенько.
Например, что делать, если пришло время нового расчета, а стоит блокировка клиентом? Или как ловить окно расчета, если клиентов много и они практически непрерывно блокируют таблицу.
А собственно почему нельзя просто партиционировать таблицу, и удалять а затем подключать новые партиции рассчитаные заранее?
Мы же говорим про DWH, как же там без партиционирования?!
Опять же такой механизм позволяет гибко работать не только подменяя всю таблицу, но и ее часть за конкретный период например.
Для приложения ничего не меняется, все блокировки PostgreSQL сам сделает
Если честно, уже устал на один и тот же вопрос отвечать.
Потому что тогда все процедуры формирующие такие таблицы потребуется переписывать на динамический SQL, что намного более трудоемко, чем те несколько строк переименования таблиц и индексов, которые приведены в статье.
А преимуществ такой подход не дает никаких, так как манипуляции с секционированными таблицами так же потребуют монопольных блокировок.
Для приложения ничего не меняется, все блокировки PostgreSQL сам сделает
А как Вы собрались выбирать именно из нужной секции? Это можно сделать или через представление, которое нужно пересоздавать, или через дополнительную таблицу, содержащую ключ актуальной секции. Что опять таки сложнее, чем вариант описанный в статье.
Извините, но вы видимо не поняли:
Есть таблица A, она партиционирована, скажем партиции A1 и A2 (можно и одну партицию A1, что бы был ваш случай). Все приложения работают с A и про партиции ничего не знают, оптимизатор сам решает какие партиции брать для запроса.
Теперь готовим отдельно таблицу A3, индексируем ее, далее отключаем/удаляем скажем A1 и подключаем A3. Пользователи как работали с A, так и работают, никаких кастомных скриптов, штатные DETACH PARTITION и ATTACH PARTITION.
Зачем представление и дополнительная таблица мне не понятно.
Партиционирование и подключение/отключение партиций это стандартный паттерн для DWH, зачем изобретать велосипед с переименованием таблиц мне не понятно.
Теперь готовим отдельно таблицу A3,
Сегодня мы готовим таблицу A3. А завтра тоже A3? Но она ведь уже есть! Значит завтра нам надо готовить уже таблицу A1. И как без динамического SQL Вы собрались это делать одними и теми же процедурами?
Пользователи как работали с A, так и работают
А как теперь выбирать данные из A3, а не A1? Вы знаете способ, как для A3 и A1 указать одинаковый FOR VALUES? Или Вы собрались секционировать по фиктивному ключу, приводя оптимизатор запросов в ступор из-за его отсутствия в запросах клиентов?
стандартный паттерн для DWH
Да, "золотой молоток" называется. Но он хорош, когда данные пополняются, а не полностью замещаются.
Еще детальнее:
готовим a_tmp
старую партицию a1 отключаем/удаляем
новую подключаем с ТЕМ ЖЕ FOR VALUES
переименовываем a_tmp в a1, на пользователей не влияет, они работают с A
Ключ партиционирования можно взять любую колонку в таблице, например id, а лучше дату операции (тогда сможем выделить несколько партиций и ускорится), ступора у оптимизатора не будет, даже если нет ключа партиционирования в запросе, не придумывайте
Attach partition именно способ для быстрой замены данных, а не пополнения.
Теперь про ваш способ: вместо стандартных SQL команд, получилось "яйцо фаберже", с которым ничего сделать нельзя, партиционировать и менять часть данных - нельзя, вторичные ключи на него создать - нельзя.
Придут люди вам на замену и будут разбираться с экзотическим кодом.
старую партицию a1 отключаем/удаляем
Перед этим уже потребуется advisory lock, чтобы клиенты не считали пустоту, вместо данных.
новую подключаем с ТЕМ ЖЕ FOR VALUES
Вот только если DETAСH уже позволяет CONCURRENTLY, то ATTACH - никак.
переименовываем a_tmp в a1, на пользователей не влияет, они работают с
И чем это отличается от переименовывания A_shadow в A, как в статье? Причем все те же проблемы, требующие переименования индексов сохраняются.
с которым ничего сделать нельзя, партиционировать и менять часть данных - нельзя, вторичные ключи на него создать - нельзя.
Почему же? Все тоже самое. Откуда Вы эти ограничения взяли?
Ключ партиционирования можно взять любую колонку в таблице, например id, а лучше дату операции
А этого вообще не понял. Раз речь идет о таблице, которая полностью переписывается ежедневно, какая там дата операции? В моем случае она одна на всю группу таблиц участвующих в формировании и трансформации временных серий и результатах прогнозирования.
Простой вопрос: а что мешает сделать максимально просто и даже тупо: открыть транзакцию с уровнем изоляции REPEATABLE READ, удалить все старые данные из таблицы, залить туда новые и зафиксировать транзакцию? Ведь если клиенты не изменяют таблицу, а только читают из нее, то они, судя по описанию уровней изоляции в документации, будут видеть либо только старые данные, либо только новые, в зависимости от того, началась ли их читающая транзакция до фиксации обновляющей транзакции или после.
PS Вопрос связан с тем, что в древние времена с Interbase, который изначально имел многоверсионное хранилище, как и современный (ну как современный - четверть века, вроде как) PostgreSQL, такие приемы работали на ура.
Это приведет к огромному увеличению нагрузки на БД. Во-первых, заливка данных будет происходить в индексированную таблицу. Во-вторых, удаление данных из таблицы многократно более тяжелая операция, чем удаление самой таблицы. В третьих, после фиксации этой транзакции VACUUM потребуется физически зачистить все удаленные строки в таблице.
То есть, так можно делать, но предложенный в статье способ намного эффективней.
Соображения за ваш варинат понятны, но есть и соображения против. У PosgreSQL поддержка снимков ограничена AFAIK: выполнение операторов DDL с изоляцией через снимок не поддерживатся, операторы DDL, типа ALTER TABLE, требуют накложения на таблицу эксклюзивной блокировки (в Interbase, кстати, по крайней мере версии 4, ЕМНИП, DDL тоже поддерживался через снимки). И если у вас имеются долгие транзакции на чтение из этой таблицы, да ещё и перекрывающиеся друг с другом, то улучить момент для переименования может быть трудно, вплоть до невозможности. Но это уже надо смотреть по месту, для конкретной базы и конкретного профиля нагрузки.
Недостатки альтернативного способа - удаление и заливка данных в одной транзации без операторов DDL - тоже надо смотреть по месту. Например - как много индексов у таблицы: если немного, то замедление в пару разу может быть некритичным. А что до VACUUM, то он может выполняться и в параллель с нормальной работой (и выполняется - автооочисткой) и тут тоже надо смотреть конкретно, успевает ли он подчистить удаленные записи за разумное время. Ну, и статистику пересчитать, чтобы запросы не слишком неоптимальными после очистки были.
Но в целом ваш вариант, как вариант выбора, несомненно заслуживает внимания. Благодарю за статью.
У PosgreSQL поддержка снимков ограничена
Эта проблема решается в статье advisory lock.
как много индексов у таблицы
Это мелочи по сравнению с удалением записей из таблицы, вместо удаления всей таблицы. Прикиньте нагрузку на WAL.
VACUUM, то он может выполняться и в параллель с нормальной работой
Но это все равно нагрузка на БД. В первую очередь на диск. Ведь ему в Вашем сценарии нужно переместить миллиард записей на место мертвых кортежей. И пока он это не сделает, таблица на диске будет занимать двукратный объем.
Эта проблема решается в статье advisory lock.
Использование явной блокировки фиктивного ресурса (advisory lock) взамен неявной реальных ресурсов- оно, конечно полезно: и требование всё делать в одной транзакции снимает, и от deadlock (который иначе не исключен, т.к. требуется блокировка нескольких ресурсов - таблицы и индексов) гарантирует. Но вот проблему с описанным выше сценарием - потоком разделяемых блокировок, мешающим получить эксклюзивную - она не решает. Точнее, решает только при политике, запрещающей выдавать разделяемые блокировки на ресурс при наличии ожидающего запроса на эксклюзивную. Но у такая политика - она, вообще-то, мешает выполнить параллельно те запросы, которые при выдаче блокировки чисто по состоянию ресурса выполнить было бы можно. Так что используется ли в PostgreSQL такая политика (тем более - по умолчанию) - это для меня нерешенный вопрос - в документации верхнего уровня по блокировкам про это не сказано, глубже я не лазил, а из общих соображений использовать такую политику по умолчанию - это сомнительное решение.
Это мелочи по сравнению с удалением записей из таблицы, вместо удаления всей таблицы. Прикиньте нагрузку на WAL.
Ну, нагрузка на WAL - в который запись идет последовательно, что быстрее (на HDD - сильно быстрее) - она всяко меньше чем нагрузка из-за записи в само хранилище пометок о том, что записи после указанной транзакции больше не с нами (детали чего и сколько для этого писать не смотрел). Насчет мелочи это или нет - не знаю: последовательная вставка в записей дерево индекса (особенно - в порядке, постоянно вызывающем его перебалансировку) - это тоже не подарок, создать дерево с нуля при создании индекса может быть сильно лучше. Но нагрузка таки действительно больше, да. А насколько это критично - это надо смотреть по месту, как я уже написал.
Но это все равно нагрузка на БД. В первую очередь на диск. Ведь ему в Вашем сценарии нужно переместить миллиард записей на место мертвых кортежей.
Дык я писал не про VACUUM FULL (который, кстати, автоочистка AFAIK не делает), а про просто VACUUM - который ничего никуда не перемещает, а только помечает ненужные уже удаленные записи как свободное пространство.
И пока он это не сделает, таблица на диске будет занимать двукратный объем.
И пусть занимает. Все равно этот объем дискового пространства должен быть зарезервирован для следующей перезаливки данных в таблицу. А если так, то какая разница где он будет зарезервирован - в свободном пространстве файловой системы или в свободном пространстве внутри файла, принадлежащего БД?
Так что используется ли в PostgreSQL такая политика (тем более - по умолчанию) - это для меня нерешенный вопрос
Держите решение https://dev.to/bolajiwahab/2022-01-13-postgresql-lock-trees-56e0
нагрузка на WAL - в который запись идет последовательно, что быстрее (на HDD - сильно быстрее) - она всяко меньше чем нагрузка из-за записи в само хранилище пометок о том, что записи после указанной транзакции больше не с нами (детали чего и сколько для этого писать не смотрел)
А вот зря не смотрели. https://www.postgresql.org/docs/current/storage-vm.html
Так как в Вашем примере удаляются все записи, то PostgreSQL лишь переписывает *_vm файл, отмечая все страницы очищенными.
И пусть занимает. Все равно этот объем дискового пространства должен быть зарезервирован для следующей перезаливки данных в таблицу.
Как Вы чужими терабайтами разбрасываетесь )
А если подобных таблиц десяток, то все равно пусть в два раза больше места занимают?
Держите решение https://dev.to/bolajiwahab/2022-01-13-postgresql-lock-trees-56e0
Ну, мне оно без надобности. Да и большинству "разработчиков с коммерческим опытом" - тоже : нормальные веб-программисты работают любыми базами через ORM, потому что мысльдедлайн, надо фичи в прод катить.
Самое ценное в этом решении - термин "lock queue" (она же - "lock tree"): в документации на postgresql.org такого термина не найти (по крайней мере, утка - не находит). Но лучше IMHO - другая ссылка https://joinhandshake.com/blog/our-team/postgresql-and-lock-queue/ - там как раз черным по-английски написано, что запросы на блокировку PostgreSQL обрабатывает (против моих ожиданий) в порядке FIFO (как вам, вроде бы, и надо), и как с этим бороться (когда это не надо) . И если бы вы в статье про эту, недокументированную, особенность PostgreSQL написали сразу - вопроса бы не было. Ну, а я просто приму к сведению.
А вот зря не смотрели. https://www.postgresql.org/docs/current/storage-vm.html
Так как в Вашем примере удаляются все записи, то PostgreSQL лишь переписывает *_vm файл, отмечая все страницы очищенными.
Ну, не зря - я что-то подобное и предполагал. Однако, тут есть нюанс: "Visibility map bits are only set by vacuum ", т.е. при самом удалении удаленные страницы останутся видимыми, и записи в них как-то нужно пометить, чтобы транзакции, стартовавшие после фиксации удаляющей, их не видели, а вот запущенные до нее - наоборот, - и эти пометки, по логике, должны заехать и в WAL, и в файл с таблицей (детали реализации опять-таки смотреть лень). А карта видимости будет обновлена уже как-нибудь потом.
Как Вы чужими терабайтами разбрасываетесь )
Не, я другим предлагаю разбрасываться - своими терабайтами. Или - не разбрасываться, чисто на их выбор. Главное - чтобы выбор был.
PS Думаю, пора кончать это бзанудство, потому что на полезность вашей статьи оно не влияет, а кого с этими нюансами припрет (а это не я) - сам информацию найдет: в конце концов, есть же исходники.
И если бы вы в статье про эту, недокументированную, особенность PostgreSQL написали сразу - вопроса бы не было.
Нельзя объять необъятное. Вот Вы спросили - тогда я ответил )
записи в них как-то нужно пометить, чтобы транзакции, стартовавшие после фиксации удаляющей, их не видели
А вот тут Вы правы, а я затупил. Никуда не деться, потребуется записать xmax текущей транзакции в каждую удаляемую строку.
Немного про SLA для DWH на PostgreSQL