Глубинное устройство PostgreSQL: от архитектуры процессов до оптимизации запросов

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

1. Архитектура процессов и управление разделяемой памятью

Архитектура процессов и управление разделяемой памятью

Когда вы отправляете запрос SELECT * FROM users, для базы данных это не просто поиск строки в файле. В этот момент запускается сложнейший механизм взаимодействия десятков процессов, которые конкурируют за ресурсы, синхронизируются через системные примитивы и управляют гигабайтами оперативной памяти. PostgreSQL — это не монолитное приложение, а целая экосистема процессов, работающих в рамках модели «процесс на каждое соединение» (process-per-connection). Если в многопоточных системах (как MySQL или Oracle) ошибка в одном потоке может потенциально обрушить весь адресный столп сервера, то PostgreSQL выбирает путь изоляции, где каждый клиентский запрос обслуживается отдельным процессом ОС со своим адресным пространством.

Модель Process-per-Connection и жизненный цикл Postmaster

Основа архитектуры PostgreSQL — многопроцессорность. Центральным элементом является процесс postmaster (в современных версиях исполняемый файл называется postgres). Это «родитель» всех остальных процессов в кластере. Его основная задача — слушать сетевой порт (по умолчанию 5432), принимать входящие TCP-соединения и проводить аутентификацию.

Как только клиент успешно проходит проверку подлинности, postmaster выполняет системный вызов fork(). В результате создается новый процесс — backend process (или postgres backend), который берет на себя обслуживание конкретного сеанса. С этого момента клиент общается напрямую с выделенным ему бэкендом, а postmaster возвращается в режим ожидания новых подключений.

Использование fork() дает PostgreSQL фундаментальное преимущество в надежности. Каждый бэкенд имеет собственную виртуальную память. Если из-за программной ошибки или повреждения памяти один бэкенд аварийно завершится (segmentation fault), операционная система изолирует этот сбой. Postmaster заметит гибель дочернего процесса и, если возникнет подозрение на повреждение критических структур в разделяемой памяти, инициирует процедуру восстановления: принудительно завершит остальные бэкенды, сбросит разделяемую память и перезапустит систему.

Однако у этой модели есть цена — накладные расходы на создание процесса. Операция fork() в современных Linux-системах оптимизирована через механизм Copy-on-Write (CoW), но создание процесса все равно обходится дороже, чем создание легковесного потока. Именно поэтому при достижении порога в несколько сотен или тысяч одновременных соединений в PostgreSQL становится критически важным использование внешних пулеров соединений (например, PgBouncer или Odyssey), которые позволяют держать тысячи клиентских сессий на небольшом количестве реальных процессов бэкенда.

Иерархия вспомогательных процессов

Помимо бэкендов, обслуживающих пользователей, в системе работают «фоновые рабочие процессы» (Background Worker Processes). Они выполняют служебные задачи, без которых СУБД не смогла бы поддерживать целостность и производительность.

  • Background Writer (bgwriter): Его задача — постепенно вытеснять «грязные» (измененные) страницы из оперативной памяти на диск. Он делает это небольшими порциями, чтобы разгрузить процесс checkpointer и минимизировать всплески ввода-вывода.
  • Checkpointer: Ответственен за создание контрольных точек (checkpoints). В этот момент все измененные данные гарантированно записываются на диск, что позволяет сократить время восстановления после сбоя.
  • WalWriter: Процесс, который сбрасывает содержимое буферов журнала транзакций (WAL) на диск. Именно он обеспечивает выполнение принципа Durability в ACID.
  • Autovacuum Launcher & Workers: Система автоматической очистки. Launcher следит за состоянием таблиц и запускает воркеры для удаления старых версий строк, которые больше не видны ни одной транзакции.
  • Stats Collector: Собирает информацию о нагрузке: сколько строк прочитано, сколько индексов использовано. Эти данные критически важны для планировщика запросов.
  • Logger: Записывает сообщения об ошибках и событиях в файлы логов.
  • Эта децентрализация позволяет PostgreSQL эффективно масштабироваться на многоядерных системах, распределяя задачи по управлению ресурсами между разными единицами планирования ОС.

    Анатомия оперативной памяти: Shared Memory

    Поскольку каждый процесс в PostgreSQL имеет свое изолированное адресное пространство, им нужен механизм для обмена данными. Для этого используется разделяемая память (Shared Memory). При запуске сервера postmaster выделяет крупный сегмент памяти, к которому впоследствии подключаются все дочерние процессы.

    Разделяемая память PostgreSQL (часто называемая shared_buffers) — это сердце системы. Здесь хранятся данные, к которым обращаются все процессы. Если данных нет в shared_buffers, процесс должен пойти за ними на диск (или в кэш ОС).

    Структура Shared Buffers

    Размер этой области определяется параметром shared_buffers. Внутри она разбита на страницы фиксированного размера (по умолчанию 8 КБ). Каждая страница в памяти сопровождается дескриптором (Buffer Descriptor), который содержит метаданные:

  • Текущее состояние (грязная/чистая).
  • Счетчик ссылок (pin count) — сколько процессов сейчас читают эту страницу.
  • Usage count — популярность страницы (используется для алгоритма вытеснения).
  • Блокировки (lightweight locks), защищающие содержимое страницы.
  • Для быстрого поиска нужной страницы в огромном массиве shared_buffers используется хеш-таблица. Когда бэкенду нужен блок №42 таблицы users, он вычисляет хеш от идентификатора таблицы и номера блока, находит запись в хеш-таблице и получает указатель на слот в разделяемой памяти.

    Алгоритм вытеснения: Clock Sweep

    PostgreSQL использует алгоритм, приближенный к LRU (Least Recently Used), который называется Clock Sweep. Представьте себе циферблат часов, где каждое деление — это буфер. Стрелка «часов» постоянно движется по кругу. Когда стрелка проходит мимо буфера, она проверяет его usage_count:

  • Если , она уменьшает его на 1 и идет дальше.
  • Если и страница не «запинена» (никто её не держит), этот буфер становится кандидатом на вытеснение.
  • Если страница была изменена (стала «грязной»), перед вытеснением её необходимо записать на диск. Это одна из причин, почему bgwriter работает в фоне: он старается заранее записывать грязные страницы с низким usage_count, чтобы, когда бэкенду срочно понадобится свободный слот, ему не пришлось ждать завершения дисковой операции записи.

    Локальная память процесса (Local Memory)

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

    Work Mem

    Параметр work_mem определяет объем памяти, доступный для операций сортировки (ORDER BY, DISTINCT) и хеширования (Hash Join, хеш-агрегаты). Важный нюанс: этот лимит устанавливается не на весь запрос, а на каждый узел плана запроса.

    Если ваш запрос выполняет сложный JOIN с последующей сортировкой, он может выделить несколько порций work_mem.

    Если данных для сортировки больше, чем помещается в work_mem, PostgreSQL переходит к «внешней сортировке» (external sort), используя временные файлы на диске. Это замедляет выполнение запроса в десятки и сотни раз. Однако бездумное увеличение work_mem может привести к тому, что при всплеске нагрузки система исчерпает всю физическую память и будет убита механизмом OOM Killer в Linux.

    Maintenance Work Mem

    Этот параметр используется для административных задач: VACUUM, CREATE INDEX, ALTER TABLE. Поскольку такие операции выполняются редко и обычно по одной, значение maintenance_work_mem можно и нужно ставить значительно выше, чем work_mem. Большой объем памяти позволяет индексу строиться быстрее за счет уменьшения количества проходов по данным.

    Механизмы синхронизации: LWLocks и Spinlocks

    В среде, где сотни процессов одновременно читают и пишут в одну и ту же область памяти, неизбежно возникают конфликты. PostgreSQL не может использовать тяжелые блокировки базы данных (Heavyweight Locks) для защиты структур памяти, так как это уничтожит производительность. Вместо этого используются низкоуровневые примитивы.

    Spinlocks (Спин-блокировки)

    Это самый быстрый и примитивный вид блокировки. Процесс, который не может получить доступ к ресурсу, входит в цикл «ожидания» (зацикливается), постоянно проверяя состояние переменной. Спин-блокировки удерживаются на очень короткое время (несколько инструкций процессора). Если процесс не может получить спин-локи в течение долгого времени, это обычно признак серьезной проблемы в архитектуре или «зависания» ядра ОС.

    LWLocks (Lightweight Locks)

    Легковесные блокировки используются для защиты данных в разделяемой памяти, например, при чтении или записи страницы в буферном кэше. У них есть два режима:

  • Shared (Разделяемый): Несколько процессов могут одновременно читать данные.
  • Exclusive (Исключительный): Только один процесс может изменять данные.
  • Если процесс хочет прочитать страницу, он берет LWLock в режиме Shared. Если в это время bgwriter хочет сбросить эту страницу на диск, он запросит Exclusive Lock и будет ждать, пока все читатели отпустят свои блокировки.

    Одной из самых известных точек раздора (contention) в PostgreSQL является WALInsertLock. Все процессы, фиксирующие транзакции, должны записать данные в WAL-буфер. Чтобы избежать перемешивания данных, они должны по очереди захватывать эту блокировку. В современных версиях PostgreSQL эта проблема частично решена через разделение WAL-буферов на несколько сегментов, но она остается классическим примером борьбы за разделяемый ресурс.

    Взаимодействие с кэшем операционной системы

    Уникальная особенность PostgreSQL — это стратегия «двойного кэширования». PostgreSQL не использует O_DIRECT (прямой ввод-вывод в обход кэша ОС) по умолчанию. Когда данные читаются с диска, они сначала попадают в кэш страниц операционной системы (Page Cache), а затем копируются в shared_buffers.

    Это кажется избыточным, но дает ряд преимуществ:

  • Надежность: При записи данных PostgreSQL полагается на системный вызов fsync(), который гарантирует, что данные покинули кэш ОС и физически записаны на пластины диска.
  • Эффективность при чтении: Если страница вытеснена из shared_buffers, велика вероятность, что она все еще находится в кэше ОС. Чтение из памяти ядра происходит быстрее, чем физическое обращение к SSD/HDD.
  • Упреждающее чтение (Read-ahead): Операционная система умеет распознавать последовательное чтение и заранее подгружать следующие блоки в кэш, что PostgreSQL использует «бесплатно».
  • Однако это накладывает ограничения на настройку. Обычно рекомендуется выделять под shared_buffers не более 25% от общего объема оперативной памяти. Оставшуюся часть должна использовать ОС для кэширования файлов, иначе возникнет ситуация «двойного буферирования», где одни и те же данные занимают место дважды, не принося пользы.

    Динамические области памяти (DSM) и параллелизм

    Начиная с версии 9.6, PostgreSQL начал активно развивать параллельное выполнение запросов (Parallel Query). Это потребовало создания механизмов для динамического выделения разделяемой памяти (Dynamic Shared Memory, DSM).

    Когда планировщик решает, что запрос можно распараллелить, он назначает основной процесс (Leader) и несколько рабочих процессов (Workers). Чтобы рабочие процессы могли передавать лидеру промежуточные результаты (например, отсканированные строки из разных частей таблицы), PostgreSQL на лету создает временные сегменты разделяемой памяти. После завершения запроса эти сегменты уничтожаются.

    Управление DSM реализовано через различные системные механизмы в зависимости от ОС: mmap на Linux, shm_open на POSIX-системах или именованные разделяемые объекты в Windows. Это позволяет PostgreSQL гибко адаптироваться к нагрузке, не резервируя всю память заранее.

    Проблема Huge Pages

    В системах с большим объемом оперативной памяти (сотни гигабайт) стандартный размер страницы памяти в 4 КБ становится неэффективным. Таблица страниц (Page Table), которую ведет ядро ОС для отображения виртуальных адресов процессов на физические адреса, разрастается до гигантских размеров. Это приводит к промахам в TLB (Translation Lookaside Buffer) — кэше процессора для адресов памяти.

    PostgreSQL поддерживает использование Huge Pages (обычно 2 МБ в Linux). Использование Huge Pages позволяет:

  • Уменьшить размер таблицы страниц.
  • Сократить накладные расходы процессора на трансляцию адресов.
  • Зафиксировать shared_buffers в памяти, предотвращая их сброс в swap.
  • Настройка huge_pages = try или on является обязательной для высоконагруженных серверов баз данных, так как это дает прямой прирост производительности в 5-10% просто за счет более эффективной работы с «железом».

    Резюмируя архитектурный подход

    Архитектура PostgreSQL — это торжество принципа разделения ответственности. Модель процессов обеспечивает изоляцию и стабильность: если один клиент пришлет «запрос-убийцу», который вызовет падение бэкенда, остальные пользователи этого даже не заметят. Разделяемая память служит общим пространством для данных, а сложная иерархия фоновых процессов гарантирует, что эти данные будут вовремя записаны на диск и очищены от мусора.

    Понимание того, как shared_buffers взаимодействует с кэшем ОС и как work_mem ограничивает аппетиты отдельных запросов, позволяет администраторам и разработчикам не просто «крутить ручки» конфига, а осознанно балансировать между скоростью обработки данных и риском падения системы по нехватке памяти. В следующей главе мы спустимся еще глубже и разберем, как именно эти 8-килобайтные страницы данных, живущие в shared_buffers, структурированы внутри и как они преобразуются в файлы на дисковой подсистеме.

    2. Физическое хранение данных: организация файлов и структура страниц

    Физическое хранение данных: организация файлов и структура страниц

    Представьте, что вы записываете данные в таблицу с миллионами строк. Для SQL-разработчика это выглядит как бесконечный логический массив кортежей. Однако для операционной системы PostgreSQL — это всего лишь набор файлов фиксированного размера, разбитых на блоки по 8 КБ. Что происходит в тот момент, когда электрический импульс в оперативной памяти должен превратиться в устойчивое состояние магнитных доменов или ячеек NAND-памяти? Ответ кроется в строгой иерархии физического хранения: от идентификаторов баз данных в файловой системе до побайтового смещения внутри конкретной страницы.

    Иерархия объектов в файловой системе

    PostgreSQL не хранит все данные в одном гигантском файле. Вместо этого он использует древовидную структуру каталогов, которая жестко привязана к логической структуре объектов БД. Все данные кластера (экземпляра) PostgreSQL находятся в директории, которую принято называть PGDATA.

    Внутри PGDATA ключевую роль играет подкаталог base. Именно здесь располагаются данные всех баз, созданных пользователем. Каждая база данных получает свой уникальный идентификатор — OID (Object Identifier). Соответственно, путь к данным конкретной базы выглядит как base/<db_oid>.

    Внутри директории базы данных мы обнаружим файлы, имена которых также являются OID-ами таблиц и индексов. Однако здесь кроется важный нюанс: объект в PostgreSQL (например, таблица) физически может состоять из нескольких файлов, называемых сегментами. По умолчанию размер одного сегмента составляет 1 ГБ. Если таблица разрастается до 10 ГБ, в директории базы появится 10 файлов с именами вида 16384, 16384.1, 16384.2 и так далее. Это историческое ограничение связано с тем, что многие старые файловые системы плохо справлялись с файлами размером более 2 ГБ.

    Помимо основных файлов данных (main fork), у каждой таблицы могут быть вспомогательные файлы:

  • Free Space Map (FSM): файл с суффиксом _fsm. Это карта свободного пространства, которая помогает быстро найти страницу, где достаточно места для вставки новой строки.
  • Visibility Map (VM): файл с суффиксом _vm. Он хранит информацию о том, являются ли данные на странице видимыми для всех транзакций. Это критично для оптимизации процесса VACUUM и реализации Index-Only Scans.
  • Анатомия страницы: 8 килобайт порядка

    Основной единицей обмена данными между диском и оперативной памятью (Shared Buffers) является страница (page). В PostgreSQL стандартный размер страницы составляет 8192 байта (). Этот размер выбран как компромисс между эффективностью ввода-вывода и накладными расходами на управление.

    Структура страницы спроектирована так, чтобы минимизировать фрагментацию и обеспечить быстрый доступ к данным. Она состоит из пяти зон:

  • Заголовок (Page Header): занимает первые 24 байта. Содержит метаданные о самой странице (LSN последней записи в WAL, контрольную сумму, флаги, указатели на начало свободного места).
  • Массив указателей (Item Identifiers / Line Pointers): растет сверху вниз сразу после заголовка. Каждый указатель занимает 4 байта и содержит информацию о смещении данных строки на странице и их длине.
  • Свободное пространство (Free Space): нераспределенная область в середине страницы.
  • Данные (Items / Tuples): сами строки таблицы, которые записываются в конец страницы и растут снизу вверх (навстречу указателям).
  • Специальная область (Special Space): опциональная зона в самом конце страницы, используемая в индексах для хранения специфических данных (например, указателей на соседние листы в B-tree). В обычных таблицах эта область пуста.
  • Заголовок страницы и его поля

    Заголовок страницы определяется структурой PageHeaderData. Рассмотрим наиболее важные поля:

  • pd_lsn: Log Sequence Number последней порции WAL-лога, которая изменила эту страницу. Это критически важно для восстановления данных: PostgreSQL не может записать страницу на диск, пока соответствующий WAL-запись не будет сброшена (протокол WAL).
  • pd_checksum: контрольная сумма страницы. Позволяет обнаружить повреждения данных на уровне дисковой подсистемы.
  • pd_lower: смещение (в байтах) до начала свободного места (конец массива указателей).
  • pd_upper: смещение до конца свободного места (начало данных строк).
  • Когда pd_lower встречается с pd_upper, страница считается заполненной. Однако PostgreSQL умеет проводить "дефрагментацию" внутри страницы, если строки были удалены, сдвигая оставшиеся данные и объединяя свободные фрагменты в единый блок.

    Структура строки (Tuple)

    Строка в PostgreSQL — это не просто набор пользовательских данных. Это сложный объект, несущий на себе "груз" многоверсионности (MVCC). Каждая версия строки (tuple) снабжается заголовком HeapTupleHeaderData, который занимает минимум 23 байта.

    Основные поля заголовка строки:

  • t_xmin: ID транзакции, которая создала (вставила) эту версию строки.
  • t_xmax: ID транзакции, которая удалила или обновила эту строку. Если строка актуальна, это поле равно 0.
  • t_cid: Command Identifier, порядковый номер команды внутри транзакции.
  • t_ctid: физический адрес строки (номер страницы и индекс в массиве указателей). Если строка была обновлена, t_ctid указывает на новую версию строки (даже если она на другой странице). Это основа цепочек обновлений.
  • t_infomask: битовая маска, описывающая состояние строки (имеет ли она NULL-значения, была ли она зафиксирована (committed) или отменена (aborted)).
  • После заголовка идет Bitmap NULL-значений. Если в таблице много столбцов, допускающих NULL, PostgreSQL использует компактную битовую карту, чтобы не тратить место на хранение пустых полей. И только после этого начинаются реальные пользовательские данные.

    Выравнивание данных (Alignment)

    Важный аспект, о котором часто забывают: PostgreSQL выравнивает данные в памяти и на диске. Большинство типов данных требуют выравнивания по границе 4 или 8 байт. Например, если у вас есть столбец char(1) (1 байт) и за ним идет bigint (8 байт), PostgreSQL вставит 7 "пустых" байт (padding) между ними, чтобы bigint начинался с адреса, кратного 8. Это необходимо для корректной работы процессора с памятью. Неправильное проектирование порядка столбцов может привести к потере до 20-30% дискового пространства на пустых пропусках.

    Механизм TOAST: хранение больших объектов

    Поскольку размер страницы фиксирован (), PostgreSQL не может сохранить строку, размер которой превышает этот лимит (с учетом заголовков). Для решения этой проблемы используется технология TOAST (The Oversized-Attribute Storage Technique).

    Как только размер строки превышает порог (обычно это 2 КБ), PostgreSQL пытается сжать длинные поля. Если сжатие не помогает довести размер строки до приемлемого, "длинные" данные выносятся в отдельную вспомогательную таблицу — TOAST-таблицу.

    В основной таблице вместо реальных данных остается 18-байтовый указатель (pointer), который содержит:

  • OID записи в TOAST-таблице.
  • Размер данных (сжатый и исходный).
  • В самой TOAST-таблице данные разбиваются на куски (chunks) размером около 2 КБ. Каждому куску присваивается порядковый номер. При чтении основной таблицы PostgreSQL прозрачно для пользователя собирает все куски из TOAST-таблицы, разжимает их и возвращает результат.

    > "TOAST is the best thing since sliced bread" > > PostgreSQL Documentation

    Важно понимать, что индексы на TOAST-таблицы создаются автоматически, что обеспечивает быстрый поиск чанков. Однако использование TOAST накладывает накладные расходы на чтение, поэтому для сверхвысоких нагрузок иногда лучше хранить большие объекты во внешних хранилищах (например, S3), оставляя в БД только ссылки.

    Уровни абстракции: Tablespaces

    Хотя по умолчанию данные лежат в PGDATA/base, PostgreSQL позволяет распределять данные по разным физическим дискам с помощью табличных пространств (Tablespaces).

    Создание Tablespace — это, по сути, создание символической ссылки из директории PGDATA/pg_tblspc на произвольный путь в файловой системе. Это полезно для:

  • Выноса горячих данных (индексов или активных таблиц) на быстрые NVMe-накопители.
  • Перемещения архивных данных на медленные и дешевые HDD.
  • Разделения нагрузки между разными RAID-массивами.
  • С точки зрения структуры страниц, данные в альтернативных Tablespace ничем не отличаются от данных в base. Разница лишь в префиксе пути к файлу.

    Взаимодействие с файловой системой и кэширование

    Когда PostgreSQL запрашивает чтение страницы, происходит следующий каскад событий:

  • Shared Buffers: Проверяется, нет ли страницы в разделяемой памяти PostgreSQL.
  • OS Page Cache: Если в буферах страницы нет, процесс делает системный вызов read(). Операционная система проверяет свой собственный кэш страниц.
  • Disk I/O: Если и в кэше ОС данных нет, происходит реальное чтение с диска.
  • Этот феномен называется двойным кэшированием. PostgreSQL намеренно не использует O_DIRECT (прямой доступ к диску в обход кэша ОС) в большинстве случаев, полагаясь на продвинутые алгоритмы упреждающего чтения (read-ahead) и вытеснения, реализованные в ядре Linux. Однако это требует от администратора правильной настройки: параметр shared_buffers обычно рекомендуется устанавливать в размере 25% от общей оперативной памяти, оставляя остальное под кэш ОС.

    Целостность данных: контрольные суммы и Full Page Writes

    Физическое хранение неразрывно связано с надежностью. Одной из самых опасных проблем является частичная запись страницы (torn page). Это происходит, когда операционная система или диск падает в момент записи 8-килобайтной страницы, и на диске оказывается "каша": часть страницы старая, часть новая.

    PostgreSQL борется с этим двумя способами:

  • Data Checksums: Если они включены (параметр data_checksums), каждая страница при чтении проверяется на соответствие заголовку. Если сумма не совпадает, PostgreSQL остановит работу с ошибкой, предотвращая дальнейшее повреждение данных.
  • Full Page Writes (FPW): После каждой контрольной точки (checkpoint) первое изменение любой страницы приводит к тому, что в WAL-лог записывается не маленькая дельта изменений, а вся страница целиком (все 8 КБ). В случае сбоя и обнаружения "разорванной страницы", PostgreSQL просто восстановит её из WAL.
  • Практический пример: расчет физического размера

    Допустим, у нас есть таблица:

    Рассчитаем минимальный размер одной строки:

  • Заголовок строки: 23 байта.
  • Выравнивание заголовка: до 24 байт.
  • id (bigint): 8 байт.
  • ts (timestamp): 8 байт.
  • value (float8): 8 байт.
  • status (char): 1 байт.
  • Padding (выравнивание): 7 байт (чтобы следующая строка началась с границы 8 байт).
  • Итого: . На одной странице объемом 8192 байта (минус заголовок страницы 24 байта) поместится примерно строк (где 4 — это размер указателя в массиве указателей).

    Если мы изменим порядок столбцов на (status, id, ts, value), то между status и id возникнет 7 байт пропуска. В данном конкретном случае размер не изменится из-за финального выравнивания всей строки, но в более сложных схемах перестановка fixed-length полей перед variable-length или группировка полей по размеру (8, 4, 2, 1) может существенно сэкономить место.

    Жизненный цикл страницы на диске

    Когда мы выполняем DELETE, PostgreSQL не удаляет данные физически и не уменьшает размер файла. Он лишь помечает версию строки как удаленную в t_xmax. Место на странице становится "мертвым" (dead tuples). Только процесс VACUUM сможет пометить это место как свободное в FSM (Free Space Map), чтобы будущие операции INSERT могли его переиспользовать.

    Физическое уменьшение файла (сжатие таблицы) возможно только через VACUUM FULL или CLUSTER, что требует эксклюзивной блокировки всей таблицы, так как подразумевает полную перезапись файлов и перестроение всей структуры страниц.

    Понимание физического уровня PostgreSQL позволяет инженерам не только оптимизировать запросы, но и правильно выбирать аппаратное обеспечение, настраивать параметры файловой системы (например, размер блока или параметры монтирования) и проектировать схемы данных, которые будут эффективно использовать каждый байт дискового пространства.