pgColumnar встраивается в PostgreSQL как метод доступа к таблицерасширение ядра, определяющее, как хранятся и читаются строки таблицы и хранит данные не построчно, а по колонкам, укладывая их на диск по кривой Гильберта. Это меняет почти всё: как строки получают свои номера, когда данные становятся видимыми другим бэкендам, какие запросы ускоряются в разы, а какие замедляются. Ниже — механика по слоям, от регистрации в ядре до цифр на стенде.
Как pgColumnar встраивается в PostgreSQL
Метод доступа к таблице регистрируется SQL-командой CREATE ACCESS METHOD с указанием обработчика:
CREATE ACCESS METHOD myam TYPE TABLE HANDLER my_tableam_handler;Сам обработчик — это C-функция, объявленная как возвращающая тип table_am_handler. Это специальный тип-маркер: он сообщает PostgreSQL, что функция возвращает не обычное значение, а указатель на структуру TableAmRoutine, в которой собрано всё, что ядру нужно знать о методе доступа. Указатель обязан жить в течение всего времени работы сервера, поэтому его обычно объявляют статической константой в глобальной области и возвращают через PG_RETURN_POINTER.
TableAmRoutine — это API-структура метода доступанабор колбэков, через которые ядро вызывает метод доступа: указатели на обычные C-функции, невидимые и невызываемые на уровне SQL. pgColumnar заполняет её колбэками для создания хранилища, массовой и одиночной вставки, открытия, следующего шага и закрытия последовательного сканирования, удаления и обновления (через маску строкпометку удалённых и обновлённых строк в отдельной структуре, по которой читатель пропускает эти строки), выборки строки по item pointerидентификатору физического расположения строки, оценки размера и truncate.
Почему кривая Гильберта, а не Z-order
Обе кривые отображают многомерные координаты в одно число, но ведут себя по-разному. У кривой Гильберта расстояние между двумя последовательными точками всегда равно единице, поэтому близкие значения кривой лежат недалеко друг от друга. У Z-order наблюдаются «ямы», соответствующие размерам 128, 256 и 512.
Выбор кривой привязан к предикату: Z-order подходит для точечных поисков, а Гильберт выигрывает на range-фильтрах по нескольким столбцам, причём разрыв сокращается с ростом области запроса. При близких результатах рекомендуется измерять собственный корпус.
За выигрыш приходится платить. Выигрыш Гильберта по числу промахов мимо кэша составляет единицы процентов, а интегральное время работы примерно вдвое хуже, чем у Z-кривой, потому что вычисление кривой Гильберта объективно тяжелее. На 2D-запросах 100×100 разница по числу прочитанных страниц может достигать 10%, но почти везде она в пределах статистической погрешности, и везде кривая Гильберта ведёт себя лучше Z-кривой.
Есть и структурные трудности. Вычисление ключа Гильберта из 3 координат занимает около 1000 тактов — примерно в 20 раз больше, чем для zcurve, но авторы ориентируются на чтения буферов, так как диск дороже процессора. Для кривой Гильберта нарушен порядок: левая нижняя точка запроса не обязательно меньше правой верхней, а минимум и максимум подзапроса могут не лежать в его углах, поэтому настоящий диапазон значений может быть шире поискового запроса, вплоть до всего экстента слоя. На границах возникают «скопления» подзапросов: узкая полоска без точек режется вплоть до подзапросов размером 2 и 1, пока минимальное значение диапазона не попадёт в пределы поискового экстента. На число прочитанных страниц это не влияет — все такие проходы по B-дереву приходятся на кэш, — но объём работы впечатляет.
Как данные лежат на диске
Колоночное отношение хранит данные в собственном main fork со стандартными страницами PostgreSQL, поэтому применимы buffer manager, WAL и контрольные суммы страниц. Блок 0 — это metapageслужебная страница с указателями на текущие границы записанных данных, а начиная с блока 2 идёт логическая байтовая область. Логическое смещение отображается в номер блока и смещение внутри страницы через BYTES_PER_PAGE = BLCKSZ - SizeOfPageHeaderData.
Stripeнабор строк, буферизуемый писателем как единица хранения. Metapage хранит три high-water mark: следующий stripe id, следующий номер строки и следующее логическое байтовое смещение. Stripe id и номер строки продвигаются под эксклюзивной блокировкой буфера metapage, взятой когда писатель начинает буферизовать stripe, поэтому каждая строка получает стабильный номер и item pointer при вставке; блокировка расширения там не нужна, так как страницы данных не расширяются. Байтовое смещение продвигается отдельно при flush, когда размер stripe известен, под блокировкой расширения отношения, которую вызывающий уже держит для расширения страниц данных. Так конкурентные писатели сериализуют свои резервации, а затем пишут данные независимо.
Row group, chunk и метаданные
Row groupнабор строк, записываемый как единица записи, до stripe_row_limit строк — это единица записи. Внутри row group данные одной колонки образуют chunkпоток значений одной колонки, закодированный в векторах фиксированного размера. Вектор закрывается при достижении chunk_group_row_limit, а row group сбрасывается, когда заполняется или при pre-commit транзакции.
Метаданные хранятся в отдельных heap-таблицах каталога pgcolumnar. storage, row_group и column_chunk записывают раскладку. zone mapминимум и максимум каждого chunk и вектора, по которым скан пропускает группы хранит минимум и максимум каждого chunk и каждого вектора. bloom filterфильтр равенства на каждый chunk, проверяемый, когда диапазон min/max не исключил значение хранит фильтры равенства на каждый chunk. delete vectorметки удаления и обновления строк в row group хранит метки удаления и обновления. Zone map используется для пропуска, bloom — для проверки равенства, когда диапазон min/max не исключил значение, а delete vector загружается читателем, чтобы пропустить помеченные строки.
Crash safety
Данные живут в main fork и проходят через buffer manager и WAL, а метаданные — в WAL-логируемых heap-каталогах, поэтому таблица crash-safe так же, как любое другое отношение PostgreSQL. Ни один путь записи не обходит buffer manager и не пишет файл напрямую: log_newpage_buffer помечает страницы данных грязными и WAL-логирует их, а metapage и reserved page WAL-логируются до любого их расширения.
Метаданные вынесены в отдельный набор обычных heap-таблиц в схеме pgcolumnar (storage, row_group, column_chunk, zone_map, bloom, delete_vector, projection, free_space, options), связанных с физическим файлом через storage id. Доступ к ним идёт прямым catalog access (heap opens, index scans, inserts), а не через SPI, чтобы не зависеть от SPI reentrancy изнутри сканирования или записи.
При крахе кластера WAL replay восстанавливает каждую зафиксированную запись и отбрасывает незафиксированную. При смерти одного backend postmaster переинициализирует разделяемую память, сбрасывает остальные сессии и запускает ту же crash recovery, при этом зафиксированные данные выживают, а незафиксированная запись не оставляет видимых строк и частичной row group. Не выживает только процесс-локальное: незафиксированная работа открытой транзакции и соединения всех живых на момент краха сессий.
Версионирование формата
Каждая колоночная relation записывает две версии: native data format version (сейчас PGCN v1), которая охраняет кодирование данных, и physical metapage version, которая охраняет физическую раскладку блоков. При чтении проверяются обе: сборка отказывает в неизвестной версии с сообщением unsupported columnar format version для metapage или unsupported columnar native format version для data format, после чего чтение завершается ошибкой. Проверка выполняется на каждом пути декодирования, включая последовательное сканирование, векторный агрегат и index-scan fetch.
Каждый column chunk дополнительно несёт encoding descriptor со своей версией. Сборка читает версии дескриптора 2 и 3, а более высокую отвергает с unrecognized native encoding descriptor, причём этот отказ происходит поздно — при декодировании чанка, а не при открытии relation.
Запись: writer, кодирование и сжатие
Writer принимает строки и раскладывает их по per-column буферам, присваивая каждой строке зарезервированный номер строки и item pointer. Вектор — это фиксированные 1024 значения; когда он заполнен, writer его закрывает. Когда row group заполнен, кодирование и сжатие обрабатывают каждый столбец, модуль хранения пишет страницы, а модуль метаданных записывает результат. Оставшиеся буферы сбрасываются в конце оператора и перед коммитом.
При сбросе row group кодируется каждый column chunk с каскадом кодирований по вектору, вычисляются zone maps и per-chunk bloom filters, и записываются строки storage, row_group, column_chunk, zone_map, bloom.
Каскад кодирований
Лёгкие кодированияобратимые преобразования байтов потока значений, применяемые к каждому чанку до блочного кодека применяются к каждому чанку до блочного кодека и отменяются после декомпрессии. Набор включает RLE, frame-of-reference с bit-packing, delta и delta-of-delta для целочисленных типов, Gorilla XOR для float4/float8, словарное кодирование для низкой кардинальности и FSSTкодирование подстрок для текстовых и других varlena-значений для текстовых и других varlena-значений. Из применимых кодирований оставляется то, что даёт наименьший размер, а при декодировании оно отменяется. После этого блочный кодек (none, pglz, lz4, zstd) сжимает уже закодированный поток.
Решение о сохранении FSST принимается после работы настроенного кодека, сравнивая размеры на диске. При none тест FSST на уровне чанка пропускается, и FSST сохраняется всегда, когда был построен.
Как выбор compression меняет каскад
pgcolumnar.compression — это не только выбор блочного кодека: он также участвует в решениях о лёгких кодированиях, которые выполняются до кодека, поэтому none — это другая каскадная последовательность, а не та же самая без сжатия. Перед запуском блочного кодека каждый чанк столбца получает лёгкое кодирование, включая dictionary, run-length, delta, frame-of-reference и FSST для строк; решение о сохранении FSST принимается после того, как отработал настроенный кодек, и сравнивает то, что попадает на диск — сжатый закодированный поток. При none сравнивать не с чем: чанковый тест FSST пропускается, и FSST сохраняется всегда, когда был построен.
encode_effort = fast не выполняет поиск подстрок FSST для текстовых столбцов и других столбцов переменной длины; всё остальное остаётся тем же — dictionary, run-length, числовые схемы и блочный кодек продолжают работать, а один и тот же код читает таблицу, записанную с любым из значений, поэтому настройка управляет только стоимостью, а не совместимостью. Она снижает степень сжатия, увеличивает скорость загрузки и скорость чтения, причём величина эффекта полностью зависит от данных: для 32-символьных хешей ускорение загрузки 5.7x при 2.7% лишнего места, для e-mail 3.5x при 12.2%, для 64-символьных хешей 3.1x без лишнего места.
Полный поиск всегда стоит времени записи, но меняет хранимые байты и время чтения только тогда, когда писатель сохраняет построенную таблицу символов: он строит таблицу, сравнивает результат с тем же данным без неё после блочного кодека и сохраняет её только если экономия превышает pgcolumnar.fsst_min_gain_percent (по умолчанию 5), иначе таблица отбрасывается и столбец получает обычное кодирование. Настройка применяется к одной таблице, а не к сессии, и её стоит измерять на собственном столбце, записав его обоими способами и сравнив SELECT sum(datalength) FROM pgcolumnar.stats('your_table');.
Сериализация конкурентных уникальных вставок
Перед возвратом свежевставленной строки пути вставки table-AM берут advisory-блокировкиблокировку, которую приложение запрашивает само по произвольному числовому ключу, а не по объекту базы данных в области транзакции, ключом по значению(ям) уникального ключа строки.
Это нужно потому, что данные columnar-строки невидимы другим бэкендам до сброса страйпа в конце оператора, и проверка уникальности по btree dirty-snapshot может пропустить конфликт со строкой, ещё буферизованной в другом бэкенде. Удержание блокировки по ключу до коммита заставляет второго вставщика равного ключа ждать, пока первый закоммитит и сбросит данные, после чего обычная проверка btree обнаружит дубликат.
Равные ключи отображаются в одну блокировку хешированием каждого ключевого столбца его типовой хеш-функцией по умолчанию (согласованно с равенством индекса: масштаб numeric, регистр citext, текст с учётом collation), объединением столбцов и отображением в ограниченное число бакетов на индекс (pgcolumnar.unique_lock_buckets), чтобы ограничить бюджет блокировок. Обрабатываются уникальные, immediate, valid индексы, включая многоколоночные, частичные (предикат вычисляется для каждой строки) и выраженные индексы; индекс, чей operator class не доказуемо типовой по умолчанию, или чей ключевой тип не имеет hash proc, откатывается к одной грубой блокировке на индекс. Кэш метаданных уникальных индексов на уровне бэкенда, инвалидируемый через relcache, хранит метаданные по отношению, поэтому стоимость на строку — это хеш плюс захват блокировки.
Настройка pgcolumnar.enable_unique_insert_lock (по умолчанию on) ставит конкурентные вставки одного ключа в последовательность, используя advisory-блокировку в области транзакции, так что две вставки одного ключа корректно конфликтуют.
Чтение и ускорение запросов
Пропуск групп через zone map
Скан пропускает chunk group, когда её per-column минимум и максимум исключают предикат. Для этого нужно, чтобы условие было вида column op constant с btree-оператором сравнения, тип константы точно совпадал с типом колонки, а колонка хранилась как Parquet INT32, INT64, FLOAT или DOUBLE; текстовые, bytea, uuid и boolean колонки фильтруются, но никогда не пропускаются.
После появления delete vector агрегаты по метаданным перестают работать, потому что zone map покрывает каждую строку группы, включая удалённые: после удаления ответ по метаданным был бы неверным, и executor использует скан. Поэтому при наличии удалений в группе ответ даёт fold по декодированным значениям, по одной колонке за раз, а не метаданные. Это применяется к одной row group за раз, так что удаление затрагивает только те группы, которых оно касается, а pgcolumnar.vacuum возвращает «грязную» группу на чистый путь.
Bloom-фильтр для равенства
Bloom-фильтр — это пофильтровое дополнение к zone map: writer хеширует каждое не-NULL значение и строит фильтр на чанк, а reader проверяет его для equality-предиката, который min/max не смог отсечь, и пропускает группу, если значение доказуемо отсутствует. Хешируются только hashable-колонки, у которых collation либо отсутствует, либо детерминированная; при недетерминированной collation фильтр не строится, потому что два равных значения могут не иметь общего хеша.
Bloom дополняет min/max, поскольку min/max отсекают только диапазонные фильтры, а bloom — равенство: он работает лучше всего на кластеризованной или отсортированной колонке, где равные значения лежат в немногих chunk-группах, тогда как на разбросанной высококардинальной колонке выигрыш мал. Ложных отрицаний нет, поэтому результаты не меняются, а executor всегда повторно применяет полный квалификатор. Фильтрация включена по умолчанию через pgcolumnar.enable_bloom_filter, а подтвердить пропуск можно по счётчику Columnar Chunk Groups Removed by Filter.
Serial join runtime filter
Фильтр использует set_join_pathlist_hookхук планировщика, который вызывается при построении путей соединения и позволяет расширению добавить собственные пути для join, оборачивающий serial inner Hash Join, чей outer path является прямой колоночной скан-операцией. Координатор сначала вытягивает build side, затем прикрепляет к этому скану консервативный диапазон ключей и Bloom-фильтр. Скан отбрасывает chunk groups вне диапазона ключей build side и отвергает строки, чьи ключи отсутствуют в Bloom-фильтре этих ключей.
Group skip требует, чтобы факт-таблица была кластеризована по ключу соединения; если ключи лежат в немногих chunk groups, скан отбрасывает остальные, а если каждая группа содержит каждый ключ, group skip не удаляет ничего. Фильтр включён по умолчанию через pgcolumnar.enable_join_runtime_filter, а кластеризация выполняется через pgcolumnar.cluster или pgcolumnar.recluster.
Векторизованный агрегат
ColumnarVecSelect — это фильтр по столбцам: он превращает простые строгие условия плана вида column op const в вектор выбора по декодированной группе, используя для целочисленных, вещественных и дата/время столбцов типизированный цикл сравнения без ветвлений (стратегия btree из opfamily типа), а иначе — функцию оператора; нулевое значение или неудачное сравнение исключает строку, что соответствует семантике SQL WHERE.
Векторизованный агрегат вычисляет count, sum, avg, min и max, точно воспроизводя типы результатов PostgreSQL (целочисленная сумма как int8, целочисленное среднее как numeric, min/max по порядку типа столбца), и берётся из zone-map метаданных с откатом к scan-and-fold при наличии удалений в группе. Он выбирается только когда каждый агрегат, тип столбца и условие поддерживаются, иначе используется скалярный план.
Покрывается единственная форма без группировки: SELECT agg(col) FROM t [WHERE ...] по одной таблице или unique-key inner Hash Join, без grouping; список выборки может содержать выражения над этими агрегатами, например count(*)::text, avg(a)+avg(b), round(avg(a), 2) и max(a)-min(a).
Групповая векторизация выключена по умолчанию из-за характера отказа, а не производительности: групповой путь строит хеш-таблицу, которая не сбрасывается на диск, поэтому pgcolumnar.groupagg_max_groups ограничивает её 1 000 000 групп, и превышение лимита приводит к ошибке, а не к возврату к обычному Agg.
Кэш декодированных row group для index scan
При index scan по колоночной таблице строки выбираются по одной, и без кэша каждая выборка заново декодировала бы весь row group; поэтому модуль хранит декодированные колонки между выборками. Кэш держит до четырёх row groups на backend и действует в пределах одного statement: записи освобождаются по завершении statement, а не при commit. Объём удерживаемых декодированных байт ограничен 32 MB; при превышении колонка освобождается и далее декодируется на каждую выборку, поэтому широкий row group деградирует по одной колонке, а не сразу весь. Кэш не настраивается, и планировщик знает о лимите: он начисляет index scan стоимость декодирования, вызванного построчными выборками, чем управляет pgcolumnar.enable_index_fetch_penalty.
Для index-only scan нужны два условия: индекс содержит все колонки запроса и строки имеют отметку all-visible, которую поставляет visibility-map forkотдельная ветка хранения, где отмечается, какие row group видны всем транзакциям и не содержат удалений. VACUUM ставит отметку all-visible для row group, когда его вставляющая транзакция достаточно старая и в группе нет удалений, а любая вставка, обновление или удаление сбрасывает бит для затронутой группы. Index-only scans включены по умолчанию (pgcolumnar.enable_index_only_scan), и чтобы покрывающий запрос использовал index-only scan, нужно выполнить VACUUM после последней записи.
Параллельный скан
Планировщик строит параллельный путь для колоночного скана: при широкой проекции он выбирает его, когда max_parallel_workers_per_gather равно 4, а стоимость декодирования оценивается по ширине колонки и делится между воркерами; при значении по умолчанию 2 планировщик всё ещё выбирает последовательный план для широкого запроса.
Параллелизм помогает на широких проекциях: параллельный план выполняется примерно за 275 мс против 639 мс у последовательного, а на 50 000 000 строк четыре воркера дают близко к четырём разам на каждой форме запроса. На узкой проекции планировщик всё ещё предпочитает последовательный план, хотя параллельный быстрее, потому что «Narrow» здесь означает мало колонок, а не мало строк, и последовательный план дёшев.
Это происходит по двум причинам: планировщик взимает parallel_tuple_cost за каждую строку, переданную от воркера лидеру, фиксированную на строку и не зависящую от ширины строки, и стоимость колоночного скана низка относительно реального времени. Обходной путь — принудительно включить параллельный план через SET max_parallel_workers_per_gather и пониженный SET parallel_tuple_cost для сессии, а порог, при котором план становится параллельным, зависит от таблицы и числа воркеров.
Обслуживание: vacuum, compaction, recluster, autovacuum
Стандартный VACUUM (ручной или autovacuum) выполняет vacuum колоночной таблицы: он устанавливает биты visibility-map, используемые index-only scans, и поддерживает статистику, но не переписывает данные и не освобождает место от удалённых строк.
pgcolumnar.vacuum переписывает таблицу: материализует живые строки (читатель пропускает строки, помеченные row-mask-deleted), меняет relation на новый relfilenode, удаляет старые метаданные, записывает живые строки обратно в полные stripes и перестраивает индексы, объединяя мелкие stripes и физически освобождая место удалённых строк. Он удерживает AccessExclusiveLock на всё время выполнения, поэтому чтение и запись останавливаются.
pgcolumnar.compact работает под ShareUpdateExclusiveLock, таблица остаётся доступной, и он удаляет полностью удалённые row groups, а не переписывает частично удалённые (это делает pgcolumnar.compact_rewrite).
Удаление и старая сторона обновления устанавливают бит в записи pgcolumnar.delete_vector для row group строки, а не переписывают группу; метки накапливаются в буфере подсубтранзакции и применяются в каталог одним upsert на row group при flush, а читатель загружает delete vector row group и пропускает помеченные строки.
Recluster
pgcolumnar.recluster(table, col [, col ...]) заново устанавливает ту же Z-упорядоченную кластеризацию, что и cluster, в режиме онлайн — под ShareUpdateExclusiveLock, поэтому чтение и запись продолжаются. pgcolumnar.recluster_hilbert(table, col [, col ...]) — это онлайн-аналог cluster_hilbert и способ перевести Z-упорядоченную таблицу на кривую Гильберта.
До #614 функция переписывала всю таблицу при каждом вызове, даже если таблица уже была кластеризована по этому ключу и ничего не изменилось; эту цену платил бы каждый проход планового recluster или демона обслуживания. #614 записывает ключ кластеризации и делает избыточный вызов no-op: на неизменённой таблице из 10 000 000 строк в 67 группах чанков до #614 переписывались все 67 групп за 14 750 мс, а после #614 — 0 групп за 0,96 мс, при этом дайджест физической раскладки оставался побайтово идентичным. Первичное установление порядка в обоих случаях стоит около 21 000 мс, выигрыш в том, что второй и последующие вызовы больше не платят эту цену, что и позволяет демону обслуживания вызывать recluster по расписанию без перемалывания хранилища.
Демон pgcolumnar.autovacuum вызывает только онлайн-глаголы под ShareUpdateExclusiveLock и перекластеризует таблицу только тогда, когда у неё есть записанный ключ кластеризации от предыдущего recluster или от set_options(..., sort_by => ...).
Autovacuum
Демон обслуживания pgcolumnar.autovacuum по умолчанию выключен; когда он включён, launcher просыпается каждые pgcolumnar.autovacuum_naptime секунд (по умолчанию 60) и запускает по одному короткоживущему worker на каждую базу данных. Каждый worker спрашивает у pgcolumnar.maintenance_due(), какие колоночные таблицы пересекли порог, и затем выполняет рекомендованную операцию — compact_rewrite или recluster — через SPI, каждую в своей транзакции.
Пороги: pgcolumnar.autovacuum_compact_threshold — доля удалённых строк (по умолчанию 0.2), pgcolumnar.autovacuum_recluster_threshold — доля добавленных строк (по умолчанию 0.05); recluster выполняется только для таблицы с записанным ключом кластеризации. Демон вызывает только онлайн-операции под ShareUpdateExclusiveLock и никогда не вызывает vacuum, vacuum_sorted или cluster, поэтому не блокирует читателей и писателей. Он регистрируется как процесс вида autovacuum и отменяет собственную операцию обслуживания, как только какому-либо оператору требуется более сильная блокировка на таблице, поэтому не отбирает throughput у foreground-нагрузки.
Пограничные случаи и ограничения
Усечённый column chunk
Усечённый column chunk отвергается, а не читается, потому что chunk, чья записанная длина превышала 4 GB, приводился к 32 битам на пути извлечения индекса; из-за этого fetch мог прочитать неверные байты и выдать их как данные. Теперь оба места приведения вызывают ошибку XX001. Это описано в разделе изменений как исправление поведения. В разделе Known issues этот пункт не упоминается; там перечислены другие проблемы — рост таблицы при установке кодека на высокоэнтропийном тексте (#1074) и лишняя работа CPU при сжатии региона, который затем отбрасывается (#1075).
Какие предикаты прунят, а какие индексы бесполезны
Сканирование прунит chunk groups и вектора, читая zone maps, но только для оператора, который оно может поместить в упорядочение: оно разрешает оператор через btree operator family типа колонки и принимает пять btree-стратегий <, <=, =, >= и >. Всё остальное фильтруется после декодирования значений, и строки в обоих случаях возвращаются корректно — различается лишь объём чтения.
Исключение — перекрытие диапазонов (&&) и вхождение (@>): они прунят через второй путь, принадлежат GiST operator families, разрешаются через default GiST opclass диапазонного типа и отвечаются по статистике, которую btree minimum и maximum дать не могут. Для этого zone map дополнительно записывает greatest upper bound в каждом юните, и юнит пропускается, когда все значения заканчиваются до начала пробы или все начинаются после её конца.
GIN и BRIN строятся, но не могут быть выбраны: оба являются bitmap-only access methods, у GIN вообще нет amgettuple, поэтому единственный возможный путь — bitmap index scan, питающий bitmap heap scan, а columnar table access method не реализует bitmap-scan callback, так что планировщик не генерирует такого пути. BRIN вдобавок никогда не суммирует: brin_summarize_new_values возвращает 0 на columnar-таблице, а прямой вызов суммирования одного диапазона упирается в ERROR: columnar: partial-range index build is not supported.
RLS, row locking и устаревшие индексные записи
На колоночной таблице row-level security не применяется к функциям, которые читают или пишут её хранилище напрямую: они отказывают, если политики RLS применимы к вызывающему, и поднимают ошибку row-level security is in force on "<table>". Это касается read_projection, reconstruct_via_projection, export_arrow, export_parquet, parallel_export_parquet, import_arrow, import_parquet и parallel_copy; суперпользователь, роль с BYPASSRLS и владелец таблицы без FORCE ROW LEVEL SECURITY не отказываются, а под FORCE ROW LEVEL SECURITY владелец тоже отказывается.
Row locking через SELECT ... FOR UPDATE, FOR SHARE и FOR KEY SHARE не реализован и поднимает columnar: row locking is not supported yet; из-за этого внешний ключ не может ссылаться на колоночную таблицу, а INSERT ... ON CONFLICT DO UPDATE берёт блокировку строки и даёт ту же ошибку, тогда как ON CONFLICT DO NOTHING работает.
Устаревшие индексные записи, оставшиеся после удалений и обновлений, фильтруются при выборке и освобождаются через REINDEX, а не удаляются попутно. Удаления и старая сторона обновлений помечаются в row maskбитовой маске строк row group, где отметка означает удаление или устаревшую версию строки без перезаписи row groups, а конкурентные удаления или обновления строк в одной row group идут последовательно по записи row-mask этой группы.
parallel_copy и import/export
parallel_copy принимает только текстовый формат COPY, не CSV и не binary; целью может быть одна колоночная таблица или RANGE-секционированная таблица с колоночными секциями, а для секционированной цели файл должен быть отсортирован по возрастанию по ключу секционирования числового или дата/временного типа. Вызывающему нужны членство в роли pg_read_server_files и INSERT на цель, файл читается на хосте сервера, а max_prepared_transactions должен быть выше числа воркеров, иначе функция сразу возвращает ошибку.
Загрузка фиксируется сама, выполняется в фоновых воркерах и не входит в транзакцию вызывающего, поэтому ROLLBACK вызывающего не отменяет загруженные строки; атомарность обеспечивается двухфазной фиксацией, и сбой координатора на финальном шаге может оставить часть диапазонов зафиксированными, а часть
Где смотреть в коде
- commandprompt/pgcolumnar/docs/benchmarks.md: latency
- commandprompt/pgcolumnar/docs/ARCHITECTURE.md: model in one paragraph
- commandprompt/pgcolumnar/docs/limitations.md: scans
- commandprompt/pgcolumnar/docs/administration.md: maintenance daemon (pgcolumnar.autovacuum)
- commandprompt/pgcolumnar/docs/ARCHITECTURE.md: safety
- commandprompt/pgcolumnar/docs/benchmarks.md: stream and asynchronous IO
- commandprompt/pgcolumnar/docs/limitations.md: format stability
- commandprompt/pgcolumnar/docs/configuration.md: grouped vectorization is off by default