# Буферный кеш

Индекс LLMS: [llms.txt](/ru/llms.txt)

---

<span id="internal-ru-chapter-9" class="pdf-anchor"></span><span id="internal-ru-page-184" class="pdf-anchor"></span>

<span id="internal-ru-dummy-74" class="pdf-anchor"></span>

## 9.1. Ввод-вывод и кеширование {#internal-ru-section-9-1}

<span id="internal-ru-dummy-75" class="pdf-anchor"></span><span id="internal-ru-section-star-143" class="pdf-anchor"></span>

### Общие соображения {#internal-ru-subsection-star-144}

Операции ввода-вывода можно разделить на прямые и буферизованные.

*Прямой* ввод-вывод (Direct I/O) перемещает данные непосредственно между памятью пользовательского процесса и диском. Если при этом поддерживается прямой доступ к памяти (DMA), то такая операция даже не нагружает процессор.

*Буферизованный* ввод-вывод задействует операционную систему и ее кеш. Пользовательский процесс запрашивает данные с помощью системного вызова. Если нужные данные есть в кеше, операционная система возвращает результат немедленно; в противном случае она предварительно читает данные с диска. Так или иначе, данные сначала (возможно, заранее) помещаются в кеш в памяти ядра операционной системы, а затем копируются в память пользовательского процесса: в нашем случае — в буферный кеш PostgreSQL.

> [!NOTE]
> Хотя здесь мы говорим про оперативную память и диски, та же идея кеширования применяется для компенсации разницы в производительности любых двух типов памяти, одна из которых быстрее, но дороже и меньше по объему, а другая — медленнее, но дешевле и больше. В быстрой памяти невозможно разместить *все* данные из медленной памяти. Но в большинстве случаев активная работа ведется одновременно только с небольшой выборкой, поэтому, выделив часть быстрой памяти под хранение «горячих» данных, удается существенно экономить на обращениях к медленному

<span id="internal-ru-page-185" class="pdf-anchor"></span>

> [!NOTE]
> устройству. В современных вычислительных системах кеширование применяется повсеместно, как на программном, так и на аппаратном уровне. У одного только процессора можно насчитать три-четыре уровня кеша; свой кеш бывает также и у контроллеров дисковых массивов, и у самих дисков.

Операции ввода-вывода можно классифицировать и иначе.

При *синхронном* вводе-выводе процесс отправляет команду и блокируется. Пока операция выполняется, процесс вынужден простаивать, дожидаясь ее завершения.

При *асинхронном* вводе-выводе (Async I/O) процесс формирует и отправляет задание и продолжает работу. За результатом он возвращается позже, в удобный для себя момент. Если операция уже выполнилась, процесс получает результат сразу; если нет — блокируется до ее завершения, как при синхронном вводе-выводе.

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

Мотивацией для собственной реализации асинхронного ввода-вывода является уменьшение зависимости от операционной системы: не все системы выполняют предвыборку одинаково хорошо, а СУБД в любом случае лучше знает, какие страницы ей понадобятся и когда. Кроме того, привлекает возможность асинхронно выполнять синхронизацию данных с диском (`fsync`).

<span id="internal-ru-page-186" class="pdf-anchor"></span>

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

<span id="internal-ru-section-star-145" class="pdf-anchor"></span>

### Текущее положение дел {#internal-ru-subsection-star-146}

Работа над тем, чтобы отвязать PostgreSQL от файловой системы, началась уже несколько лет назад. (v. 18)

Прямой ввод-вывод включается параметром *debug\_io\_direct*, но пока не реализовано все необходимое, эта настройка имеет смысл только для разработчиков ядра. (*по умолчанию:* `off`) (v. 16)

Асинхронный ввод-вывод [^internal-ru-186-1] доступен пока только для чтения; запись и синхронизация по-прежнему остаются синхронными. Имеются несколько реализаций ввода-вывода, одна из которых указывается в параметре *io\_method*: можно выбрать тот способ, который поддерживается на конкретной платформе и показывает лучшие результаты в конкретной системе. (*по умолчанию:* `worker`)

- При значении `worker` ввод-вывод выполняется отдельными рабочими процессами[^internal-ru-186-2]. Этот способ работает на любых платформах и выбирается по умолчанию.

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

Количество рабочих процессов постоянно и определяется параметром *io\_workers*. На серверах с большим количеством ядер значение параметра имеет смысл увеличить (максимальное значение — 32). (*по умолчанию:* `3`)

<span id="internal-ru-page-187" class="pdf-anchor"></span>

```text {label="psql"}
=> SELECT pid, backend_type
FROM pg_stat_activity
WHERE backend_type = 'io worker';
  pid  | backend_type
-------+--------------
 55418 | io worker
 55419 | io worker
 55420 | io worker
(3 rows)
```

- При значении `io_uring` ввод-вывод выполняется самим процессом с помощью одноименного интерфейса ядра Linux и работает только в этой операционной системе[^internal-ru-187-1].

Интерфейс io\_uring использует два кольцевых буфера в памяти пользовательского процесса (отсюда и название — user-space ring): один для запросов на ввод-вывод, другой — для получения результата этих запросов. Поскольку буферы находятся в памяти процесса, взаимодействие с интерфейсом происходит без системных вызовов и не приводит к переключению контекста.

- При значении `sync` используется обычный синхронный ввод-вывод.

Интерфейс io\_uring потенциально эффективнее рабочих процессов, но есть нюансы. Помимо собственно ввода-вывода, у прочитанных страниц требуется проверять контрольные суммы, а пока ввод-вывод остается буферизованным, надо еще и копировать прочитанные данные из кеша операционной системы в буферный кеш PostgreSQL. При выборе `io_uring` эта работа выполняется самим процессом, при `worker` — распределяется между рабочими процессами. Поэтому единственный интенсивно читающий процесс, скорее всего, выиграет от способа `worker`, особенно при достаточном количестве рабочих процессов, а множество читающих процессов, порождающих массу мелких одностраничных запросов, наоборот, могут упереться в возможности ограниченного пула рабочих процессов.

Асинхронный ввод-вывод настраивается, по сути, парой значений: количеством асинхронных операций ввода-вывода, которые могут выполняться одновременно, и количеством идущих подряд страниц, которые будут читаться одной операцией. Однако параметров несколько больше.

<span id="internal-ru-page-188" class="pdf-anchor"></span>

Параметром *io\_max\_concurrency* мы задаем максимальное количество асинхронных операций в расчете на один процесс. Размер структур данных, которые создаются для асинхронного ввода-вывода в разделяемой памяти при запуске экземпляра, зависит от произведения этого значения на максимальное число процессов в системе (включая служебные). При значении по умолчанию количество выбирается автоматически исходя из размеров буферного кеша, но не превышает 64[^internal-ru-188-1]. (*по умолчанию:* `-1`)

Само по себе значение *io\_max\_concurrency* не ограничивает количество операций одного процесса. Такое ограничение устанавливают два других параметра: *effective\_io\_concurrency* для обычного чтения данных из таблиц и *maintenance\_io\_concurrency* для таких служебных задач, как, например, очистка. Установка этих параметров в ноль фактически отключает асинхронный ввод-вывод. Поскольку степень параллелизма зависит от возможностей устройства ввода-вывода, эти параметры можно устанавливать и на уровне табличных пространств. (*по умолчанию:* `16`) (*по умолчанию:* `16`)

Максимальный объем данных, который может быть прочитан одной операцией ввода-вывода, устанавливается параметром *io\_combine\_limit*; значение по умолчанию соответствует 16 стандартным 8-килобайтным страницам. Если операционная система поддерживает более крупные операции, увеличение значения может пойти на пользу OLAP-системам, в которых большие таблицы часто сканируются полностью. (*по умолчанию:* `128 kB`)

Параметр *io\_combine\_limit* можно менять на лету, но в пределах, установленных другим параметром, *io\_max\_combine\_limit*, изменение которого требует перезапуска сервера. (*по умолчанию:* `128 kB`)

Пока асинхронно выполняются только следующие операции:

- последовательное чтение (Seq Scan); (*с.* [374](/ru/18-table-access-methods/#internal-ru-page-375))

- сканирование по битовой карте (Bitmap Heap Scan); (*с.* [427](/ru/20-index-scans/#internal-ru-page-428))

- очистка и анализ таблиц, а также очистка B-деревьев и индексов GiST и SP-GiST; (*с.* [125](/ru/06-vacuum-and-freezing/#internal-ru-page-126))

- прогрев буферного кеша (расширение `pg_prewarm)`;

- <span id="internal-ru-page-189" class="pdf-anchor"></span>проверка целостности отношений (функция `verify_heapam` расширения `amcheck`).

Если асинхронный ввод-вывод отключен, при этих операциях PostgreSQL старается подсказать операционной системе глубину желаемой предвыборки, ориентируясь на значение параметра *effective\_io\_concurrency* или *maintenance\_io\_concurrency*. Также есть пара операций, которые пока не охвачены асинхронным вводом-выводом, но тоже инициируют предвыборку:

- чтение индексных страниц при удалении строк из таблицы [^internal-ru-189-1];

- чтение страниц журнала при восстановлении[^internal-ru-189-2].

Представление `pg_aios` показывает дескрипторы асинхронного ввода-вывода, которые определяют операции и хранят состояния этих операций, пока они выполняются. Впрочем, представление предназначено не для обычного мониторинга, а скорее для разработчиков ядра.

## 9.2. Устройство буферного кеша {#internal-ru-section-9-2}

Буферный кеш PostgreSQL [^internal-ru-189-3] хранит страницы отношений, сглаживая разницу между временем доступа к оперативной памяти (наносекунды) и к дискам (миллисекунды).

Буферный кеш располагается в общей памяти сервера и доступен всем процессам. Он занимает бóльшую часть общей памяти и является одной из самых важных и сложных структур данных. Понимание принципа работы кеша важно само по себе; к тому же многие другие структуры (такие как вложенные транзакции, статусы транзакций clog, журнальные записи) используют похожее, хотя и более простое кеширование.

Кеш называется буферным, потому что представляет собой массив *буферов*. Каждый буфер резервирует фрагмент памяти, достаточный для одной страницы данных и ее заголовка[^internal-ru-189-4].

<span id="internal-ru-page-190" class="pdf-anchor"></span><span id="internal-ru-figure-caption-147" class="pdf-anchor"></span>

![Устройство буферного кеша: буферный кеш; заголовок; страница](/images/internal/ru/p190-01.svg)
{#internal-ru-fig-9-1 num="9.1" caption="Устройство буферного кеша" .book-original-figure width=712 height=173}

Заголовок содержит информацию о буфере и загруженной в него странице, такую как:

- физическое расположение страницы (идентификатор файла отношения, слой и номер блока в слое);

- признак того, что данные на странице изменились и рано или поздно должны быть записаны на диск (такую страницу называют *грязной*);

- число обращений к буферу (usage count);

- счетчик закреплений буфера (pin count или reference count).

<span id="internal-ru-dummy-76" class="pdf-anchor"></span>

Чтобы получить доступ к странице данных отношения, процесс запрашивает ее у менеджера буферов [^internal-ru-190-1] и получает номер буфера, содержащего эту страницу. Процесс читает данные из страницы в кеше и там же может менять их. На время работы со страницей буфер *закрепляется*, чтобы менеджер не заменил ее на другую. При каждом закреплении увеличивается счетчик обращений к странице. Помимо закреплений, применяются и другие блокировки. (*с.* [306](/ru/15-locks-on-memory-structures/#internal-ru-page-307))

Пока страница находится в буферном кеше, работа с ней не приводит к файловым операциям.

Внутрь буферного кеша позволяет заглянуть расширение `pg_buffercache`:

```text {label="psql"}
=> CREATE EXTENSION pg_buffercache;
```

Создадим таблицу и вставим в нее одну строку:

<span id="internal-ru-page-191" class="pdf-anchor"></span>

```text {label="psql"}
=> CREATE TABLE cacheme(
  id integer
) WITH (autovacuum_enabled = off);
=> INSERT INTO cacheme VALUES (1);
```

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

```text {label="psql"}
=> CREATE FUNCTION buffercache(rel regclass)
RETURNS TABLE(
  bufferid integer, relfork text, relblk bigint,
  isdirty boolean, usagecount smallint, pins integer
) AS $$
SELECT bufferid,
  CASE relforknumber
    WHEN 0 THEN 'main'
    WHEN 1 THEN 'fsm'
    WHEN 2 THEN 'vm'
  END,
  relblocknumber,
  isdirty,
  usagecount,
  pinning_backends
FROM pg_buffercache
WHERE relfilenode = pg_relation_filenode(rel)
AND relforknumber = 0 -- только main
ORDER BY relforknumber, relblocknumber;
$$ LANGUAGE sql;
=> SELECT * FROM buffercache('cacheme');
 bufferid | relfork | relblk | isdirty | usagecount | pins
----------+---------+--------+---------+------------+------
      355 | main    |      0 | t       |          1 |    0
(1 row)
```

Страница грязная (`isdirty`), поскольку изменена и еще не записана на диск. Счетчик обращений (`usagecount`) равен единице.

## 9.3. Попадание в кеш {#internal-ru-section-9-3}

<span id="internal-ru-dummy-77" class="pdf-anchor"></span>

Когда менеджеру буферов требуется прочитать страницу[^internal-ru-191-1], он сначала пытается найти ее в буферном кеше. Для быстрого поиска нужного буфера используется хеш-таблица [^internal-ru-191-2], хранящая номера буферов.

<span id="internal-ru-page-192" class="pdf-anchor"></span>

> [!NOTE]
> Многие современные языки программирования включают хеш-таблицы как один из базовых типов данных. Часто они называются ассоциативным массивом. И действительно, с точки зрения пользователя они выглядят как массив, но в качестве индекса (который называется *ключом хеширования*) можно использовать любой тип данных, например текстовую строку, а не целое число.
>
> Диапазон возможных значений ключа может быть очень велик, но одновременно в хеш-таблице хранится не так много значений. Идея хеширования состоит в том, что значение ключа с помощью *хеш-функции* отображается в некоторое целое число. Полученное число или часть его битов используется как индекс обычного массива. Элементы такого массива называют *корзинами хеш-таблицы*.
>
> Хорошая хеш-функция распределяет ключи по корзинам равномерно, но тем не менее она может выдать одинаковые числа для разных ключей и таким образом направить ключи в одну и ту же корзину; это называется *коллизией*. Поэтому вместе со значением в корзине сохраняется и ключ хеширования, а при чтении значения по ключу перепроверяются все ключи, которые попали в одну корзину.

Известно много вариантов реализации хеш-таблиц. Для буферного кеша используется динамически расширяемая таблица c разрешением хеш-коллизий с помощью цепочек[^internal-ru-192-1].

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

> [!NOTE]
> Использование хеш-таблицы давно вызывает нарекания. Такая структура никак не помогает найти все буферы, занятые страницами определенного отношения. А это требуется при удалении (`DROP`), опустошении (`TRUNCATE`) или усечении хвостовой части таблицы во время очистки, чтобы убрать из кеша соответствующие страницы[^internal-ru-192-2]. Но хорошую замену пока никто не предложил.

Если номер буфера найден в хеш-таблице, менеджер закрепляет этот буфер и возвращает его номер процессу. После этого процесс может работать со страницей в буферном кеше, и это не приводит к вводу-выводу. (*с.* [133](/ru/06-vacuum-and-freezing/#internal-ru-page-134))

Чтобы закрепить буфер, нужно увеличить счетчик `pin count` в его заголовке; один и тот же буфер могут одновременно закрепить несколько процессов.

<span id="internal-ru-page-193" class="pdf-anchor"></span><span id="internal-ru-figure-caption-148" class="pdf-anchor"></span>

![Попадание в кеш: 3501, 0, 3; 2610, 0, 7; хеш-таблица](/images/internal/ru/p193-02.svg)
{#internal-ru-fig-9-2 num="9.2" caption="Попадание в кеш" .book-original-figure width=720 height=392}

Пока буфер закреплен (т. е. значение счетчика больше нуля), считается, что буфер используется и его содержимое не должно «радикально» измениться. Например, в странице может появиться новая версия строки (она не будет видна благодаря правилам видимости), но страница не может быть заменена на другую.

Команда `EXPLAIN` с параметрами `analyze` и `buffers` не просто показывает план запроса, но и выполняет его, а также выводит количество использованных буферов:

```text {label="psql" hl_lines="6"}
=> EXPLAIN (analyze, costs off, timing off, summary off)
  SELECT * FROM cacheme;
                   QUERY PLAN
------------------------------------------------
 Seq Scan on cacheme (actual rows=1.00 loops=1)
   Buffers: shared hit=1
 Planning:
   Buffers: shared hit=2 read=6
(4 rows)
```

Здесь `hit=1` говорит о том, что потребовалось прочитать одну страницу и она была найдена в кеше.

Закрепление буфера увеличило счетчик обращений на единицу:

<span id="internal-ru-page-194" class="pdf-anchor"></span>

```text {label="psql" hl_lines="4"}
=> SELECT * FROM buffercache('cacheme');
 bufferid | relfork | relblk | isdirty | usagecount | pins
----------+---------+--------+---------+------------+------
      355 | main    |      0 | t       |          2 |    0
(1 row)
```

Теперь обновим строку, чтобы в странице были две версии: одна актуальная, а вторая — мертвая. Это понадобится нам чуть позже.

```text {label="psql"}
=> UPDATE cacheme SET id = 2;
UPDATE 1
```

Воспользуемся тем, что открытый курсор удерживает закрепление, чтобы иметь возможность быстро прочитать следующую строку выборки. Это позволит нам увидеть закрепление в процессе работы запроса:

```text {label="psql" hl_lines="11"}
=> BEGIN;
=> DECLARE c CURSOR FOR SELECT * FROM cacheme;
=> FETCH c;
 id
----
  2
(1 row)
=> SELECT * FROM buffercache('cacheme');
 bufferid | relfork | relblk | isdirty | usagecount | pins
----------+---------+--------+---------+------------+------
      355 | main    |      0 | t       |          4 |    1
(1 row)
```

В некоторых случаях, когда требуется именно закрепленный буфер, процесс встает в очередь и засыпает до того момента, когда он сможет получить монопольный доступ к буферу. Пример такой операции — заморозка [^internal-ru-194-1]. (*с.* [134](/ru/06-vacuum-and-freezing/#internal-ru-page-135))

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

<span id="internal-ru-page-195" class="pdf-anchor"></span>

```text {label="psql" hl_lines="8 9"}
=> VACUUM VERBOSE cacheme;
INFO:  vacuuming "internals.public.cacheme"
INFO:  finished vacuuming "internals.public.cacheme": index
scans: 0
pages: 0 removed, 1 remain, 1 scanned (100.00% of total), 0
eagerly scanned
tuples: 0 removed, 2 remain, 0 are dead but not yet removable
tuples missed: 1 dead from 1 pages not removed due to cleanup
lock contention
removable cutoff: 908, which was 0 XIDs old when operation ended
new relfrozenxid: 905, which is 1 XIDs ahead of previous value
frozen: 0 pages from table (0.00% of total) had 0 tuples frozen
visibility map: 0 pages set all-visible, 0 pages set all-frozen
(0 were all-visible)
index scan not needed: 0 pages from table (0.00% of total) had 0
dead item identifiers removed
avg read rate: 0.000 MB/s, avg write rate: 137.061 MB/s
buffer usage: 33 hits, 0 reads, 3 dirtied
WAL usage: 4 records, 3 full page images, 25030 bytes, 0 buffers
full
system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
VACUUM
```

При переходе курсора на другую страницу или после его закрытия закрепление снимается. В нашем примере это происходит одновременно с окончанием транзакции:

```text {label="psql" hl_lines="5"}
=> COMMIT;
=> SELECT * FROM buffercache('cacheme') WHERE relfork = 'main';
 bufferid | relfork | relblk | isdirty | usagecount | pins
----------+---------+--------+---------+------------+------
      355 | main    |      0 | t       |          4 |    0
(1 row)
```

Так же закрепление работает и при изменении страницы. Например, добавим в таблицу еще одну строку (она попадет в ту же самую страницу):

```text {label="psql" hl_lines="5"}
=> INSERT INTO cacheme VALUES (2);
=> SELECT * FROM buffercache('cacheme') WHERE relfork = 'main';
 bufferid | relfork | relblk | isdirty | usagecount | pins
----------+---------+--------+---------+------------+------
      355 | main    |      0 | t       |          5 |    0
(1 row)
```

<span id="internal-ru-page-196" class="pdf-anchor"></span>

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

## 9.4. Промах кеша {#internal-ru-section-9-4}

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

Уберем страницу из буфера (вместо этого можно было бы очистить весь буферный кеш, перезапустив экземпляр): (v. 17)

```text {label="psql"}
=> SELECT pg_buffercache_evict(355);
 pg_buffercache_evict
----------------------
 (t,t)
(1 row)
```

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

```text {label="psql" hl_lines="6"}
=> EXPLAIN (analyze, costs off, timing off, summary off)
  SELECT * FROM cacheme;
                   QUERY PLAN
------------------------------------------------
 Seq Scan on cacheme (actual rows=2.00 loops=1)
   Buffers: shared read=1 dirtied=1
 Planning:
   Buffers: shared hit=2
(4 rows)
```

Теперь вместо `hit` команда показывает `read`, что означает промах кеша. При этом в результате запроса страница стала грязной (`dirtied`), так как запрос изменил биты-подсказки на странице. (*с.* [86](/ru/03-pages-and-tuples/#internal-ru-page-87))

Запрос к буферному кешу покажет, что счетчик использований только что добавленной страницы равен единице:

<span id="internal-ru-page-197" class="pdf-anchor"></span>

```text {label="psql" hl_lines="4"}
=> SELECT * FROM buffercache('cacheme');
 bufferid | relfork | relblk | isdirty | usagecount | pins
----------+---------+--------+---------+------------+------
      378 | main    |      0 | t       |          1 |    0
(1 row)
```

Общую статистику использования буферного кеша для таблицы можно получить из представления `pg_statio_all_tables`:

```text {label="psql"}
=> SELECT heap_blks_read, heap_blks_hit
FROM pg_statio_all_tables
WHERE relname = 'cacheme';
 heap_blks_read | heap_blks_hit
----------------+---------------
              1 |             9
(1 row)
```

Аналогичные представления есть для индексов и для последовательностей.

<span id="internal-ru-section-star-149" class="pdf-anchor"></span>

### Поиск буфера и вытеснение {#internal-ru-subsection-star-150}

<span id="internal-ru-dummy-78" class="pdf-anchor"></span><span id="internal-ru-item-5" class="pdf-anchor"></span>

Поиск подходящего буфера [^internal-ru-197-1] — непростая задача. Есть два варианта развития событий.

1. Сразу после запуска сервера кеш содержит только пустые, свободные буферы, и все они связаны в общий список.

Пока остаются свободные буферы, первый из них считается подходящим и убирается из списка.

<span id="internal-ru-item-6" class="pdf-anchor"></span>

Буфер может вернуться [^internal-ru-197-2] в список свободных буферов только в том случае, когда страница исчезает, а не заменяется другой страницей. Это происходит при удалении (`DROP`), опустошении (`TRUNCATE`) или усечении хвостовой части таблиц во время очистки. (*с.* [133](/ru/06-vacuum-and-freezing/#internal-ru-page-134))

2. <span id="internal-ru-page-198" class="pdf-anchor"></span>Рано или поздно свободные буферы заканчиваются (поскольку обычно размер базы данных превышает объем памяти, выделенной под кеш), и тогда менеджер должен выбрать один из занятых буферов и вытеснить находящуюся там страницу. Для этого используется «часовой» алгоритм (clock sweep). Имеется «часовая стрелка», указывающая на один из буферов. Стрелка пробегает буферный кеш по кругу, уменьшая счетчики обращений к страницам на единицу. Поиск завершается, когда стрелка доходит до незакрепленного буфера с нулевым счетчиком.

С одной стороны, счетчик обращений увеличивается каждый раз, когда буфер используется (закрепляется). С другой — уменьшается при поиске буфера для вытеснения. Таким образом, в первую очередь вытесняются те страницы, к которым реже обращались в последнее время, а активно используемые страницы остаются в кеше.

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

После того как буфер найден, надо удалить из хеш-таблицы ссылку на старую страницу, еще находящуюся в буфере.

Но если буфер оказался грязным, то есть содержит измененные данные, старую страницу нельзя просто выбросить — сначала менеджер буферов сохраняет ее на диск. (*с.* [226](/ru/10-write-ahead-log/#internal-ru-page-227))

<span id="internal-ru-figure-caption-151" class="pdf-anchor"></span>

![Поиск буфера и вытеснение: свободные буферы; «часовая стрелка»](/images/internal/ru/p198-03.svg)
{#internal-ru-fig-9-3 num="9.3" caption="Поиск буфера и вытеснение" .book-original-figure width=715 height=310}

<span id="internal-ru-page-199" class="pdf-anchor"></span>

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

> [!NOTE]
> СУБД, использующие прямой ввод-вывод и не зависящие от кеша операционной системы, различают логические чтения (из оперативной памяти, то есть из буферного кеша) и физические чтения (с диска). С точки зрения PostgreSQL страница либо прочитана из буферного кеша, либо запрошена у операционной системы, но у СУБД нет возможности узнать, была ли она в последнем случае найдена в оперативной памяти или прочитана с диска.

В хеш-таблицу помещается ссылка на новую страницу, а буфер закрепляется. Это приводит к увеличению счетчика обращений, то есть к установке его в единицу. В итоге у буфера появляется время нарастить счетчик обращений, пока «часовая стрелка» обходит по кругу буферный кеш.

## 9.5. Массовое вытеснение {#internal-ru-section-9-5}

При операциях, выполняющих массовое чтение или запись данных, есть опасность быстрого вытеснения полезных страниц из буферного кеша «одноразовыми» данными.

<span id="internal-ru-dummy-79" class="pdf-anchor"></span>

Чтобы этого избежать, для массовых операций в буферном кеше используются относительно небольшие *буферные кольца* (buffer ring), и вытеснение происходит в их пределах, не затрагивая остальные буферы.

> [!NOTE]
> Наряду с термином «buffer ring» в коде используется и «ring buffer», то есть «кольцевой буфер». Но такой перевод вызвал бы путаницу, поскольку сам (кольцевой) буфер состоит из буферов (буферного кеша). Термин «кольцо» лишен такого недостатка.

Буферное кольцо заданного размера представляет собой массив буферов, которые используются «по кругу», один за другим. Сначала буферное кольцо пусто, и буферы набираются в него из буферного кеша обычным образом. А затем начинает действовать вытеснение, но только внутри кольца[^internal-ru-199-1].

<span id="internal-ru-page-200" class="pdf-anchor"></span>

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

Существует три стратегии вытеснения[^internal-ru-200-1].

**Стратегия массового чтения (bulkread)** применяется при последовательном сканировании больших таблиц, размер которых превышает четверть буферного кеша. (*с.* [374](/ru/18-table-access-methods/#internal-ru-page-375))

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

Если в момент начала сканирования таблицы процесс обнаруживает, что эта таблица уже сканируется, он присоединяется к существующему буферному кольцу и получает часть таблицы без избыточного ввода-вывода[^internal-ru-200-2]. Когда первый процесс заканчивает сканирование, второй отдельно дочитывает пропущенное начало таблицы.

Кольцо должно быть достаточно большим, чтобы буферы успевали освобождаться до того, как потребуются повторно, и задерживались в кольце достаточно долго, чтобы несколько читающих процессов могли синхронизироваться. При асинхронном вводе-выводе одновременно могут выполняться *effective\_io\_concurrency* операций, каждая из которых может читать *io\_combine\_limit* байт, что и определяет необходимый размер. Если асинхронный ввод-вывод отключен, произведение обратится в ноль; на этот случай добавляется еще 256 Кбайт (32 стандартные страницы). Но чтобы кольцо не получилось уж слишком большим, его размер ограничен отношением объема буферного кеша к максимальному количеству процессов в системе: кеша должно хватить на всех. (v. 18) (*с.* [183](/ru/09-buffer-cache/#internal-ru-page-184)) (*по умолчанию:* `16`) (*по умолчанию:* `128K`)

Таким образом, с настройками по умолчанию — при достаточном большом буферном кеше — размер кольца будет составлять 32 + 16 ⋅ 16 = 288 буферов.

<span id="internal-ru-page-201" class="pdf-anchor"></span>

**Стратегия массовой записи (bulkwrite)** применяется для операций `COPY FROM`, `CREATE TABLE AS SELECT`, `CREATE MATERIALIZED VIEW` и тех вариантов `ALTER TABLE`, которые вызывают перезапись таблицы. Выделяется довольно большое кольцо размером 16 Мбайт (2048 стандартных страниц), но не больше \(\frac18\) от размера всего буферного кеша.

**Стратегия очистки (vacuum)** применяется процессами очистки и анализа при полном сканировании таблиц без учета карты видимости. Размер кольца определяется параметром *vacuum\_buffer\_usage\_limit*, но может быть переопределен в команде `VACUUM` или `ANALYZE`. (*по умолчанию:* `2MB`) (v. 16)

Буферные кольца не всегда защищают от ненужного вытеснения. При изменении большого числа строк командами `UPDATE` или `DELETE` таблица сканируется, используя стратегию массового чтения, но из-за того, что страницы изменяются, буферное кольцо вырождается и фактически не работает.

Другой важный случай — хранение в базе данных больших значений, для которых используется TOAST. Доступ к toast-таблице всегда происходит по индексу, несмотря на потенциально большой объем читаемых данных, и, следовательно, не использует буферное кольцо. (*с.* [33](/ru/01-introduction/#internal-ru-page-34))

Посмотрим теперь на стратегию массового чтения. Для упрощения расчетов создадим таблицу так, чтобы при вставке одна строка занимала целую страницу. Размер буферного кеша по умолчанию — 16 384 страницы по 8 Кбайт. Значит, чтобы сканирование таблицы использовало буферное кольцо, таблица должна занимать больше 4096 страниц.

```text {label="psql"}
=> CREATE TABLE big(
  id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
  s char(1000)
) WITH (fillfactor = 10);
=> INSERT INTO big(s) SELECT 'FOO' FROM generate_series(1,4096+1);
```

Проанализируем таблицу:

<span id="internal-ru-page-202" class="pdf-anchor"></span>

```text {label="psql"}
=> ANALYZE big;
=> SELECT relname, relfilenode, relpages
FROM pg_class
WHERE relname IN ('big', 'big_pkey');
 relname  | relfilenode | relpages
----------+-------------+----------
 big      |       16569 |     4097
 big_pkey |       16575 |       14
(2 rows)
```

Очистим кеш от табличных страниц, прочитанных во время анализа, после чего прочитаем всю таблицу: (v. 18)

```text {label="psql" hl_lines="10"}
=> SELECT pg_buffercache_evict_relation('big'::regclass);
 pg_buffercache_evict_relation
-------------------------------
 (4101,4099,0)
(1 row)
=> EXPLAIN (analyze, costs off, timing off, summary off, buffers off)
SELECT * FROM big;
                  QUERY PLAN
-----------------------------------------------
 Seq Scan on big (actual rows=4097.00 loops=1)
(1 row)
```

Размер буферного кольца ограничен из-за небольшого объема буферного кеша — табличными страницами в нем занято только 94 буфера вместо ожидаемых 288:

```text {label="psql"}
=> SELECT count(*)
FROM pg_buffercache
WHERE relfilenode = pg_relation_filenode('big'::regclass);
 count
-------
    94
(1 row)
```

Если же таблица читается с помощью индекса, то буферное кольцо не используется:

```text {label="psql" hl_lines="6"}
=> EXPLAIN (analyze, costs off, timing off, summary off, buffers off)
SELECT * FROM big
ORDER BY id;
                           QUERY PLAN
----------------------------------------------------------------
 Index Scan using big_pkey on big (actual rows=4097.00 loops=1)
   Index Searches: 1
(2 rows)
```

<span id="internal-ru-page-203" class="pdf-anchor"></span>

В результате в буферном кеше оказывается вся таблица и весь индекс:

```text {label="psql"}
=> SELECT relfilenode, count(*)
FROM pg_buffercache
WHERE relfilenode IN (
  pg_relation_filenode('big'), pg_relation_filenode('big_pkey')
)
GROUP BY relfilenode;
 relfilenode | count
-------------+-------
       16569 |  4097
       16575 |    14
(2 rows)
```

## 9.6. Настройка размера {#internal-ru-section-9-6}

Размер буферного кеша определяется параметром *shared\_buffers*. Значение по умолчанию заведомо занижено; имеет смысл увеличить его сразу же после установки PostgreSQL. Изменение параметра требует перезапуска, поскольку общая память выделяется под кеш при старте сервера. (*по умолчанию:* `128MB`)

Из каких соображений выбирать подходящее значение?

Даже самая большая база имеет ограниченный набор «горячих» данных, с которыми ведется активная работа в каждый момент времени. В идеале именно этот набор и должен помещаться в буферный кеш (конечно, с запасом под неизбежные «одноразовые» данные). Если размер кеша будет меньше, то активно используемые страницы будут постоянно вытеснять друг друга, создавая избыточный ввод-вывод. Но и бездумно увеличивать кеш тоже неправильно, поскольку это повышает накладные расходы на его поддержание, а оперативная память требуется и для других нужд.

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

<span id="internal-ru-page-204" class="pdf-anchor"></span>

Надо также принимать во внимание, что — пока не включен прямой ввод-вывод — промах кеша PostgreSQL не обязательно приводит к физическому вводу-выводу. При небольшом буферном кеше свободное место будет задействовано под кеш операционной системы, что может в определенной степени сглаживать проблему. Но, в отличие от СУБД, операционная система ничего не знает о смысле прочитанных данных, поэтому использует другую стратегию вытеснения.

Стандартная рекомендация — взять в качестве первого приближения \(\frac14\) оперативной памяти, а дальше смотреть по ситуации.

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

Но некоторый анализ все же возможен.

Во-первых, с помощью уже знакомого расширения `pg_buffercache` можно смотреть на текущее состояние кеша под разными углами. Например, изучить распределение буферов по степени их использования:

```text {label="psql"}
=> SELECT usagecount, count(*)
FROM pg_buffercache
GROUP BY usagecount
ORDER BY usagecount;
 usagecount | count
------------+-------
          1 |  4148
          2 |   146
          3 |    21
          4 |    15
          5 |   209
            | 11845
(6 rows)
```

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

Можно посмотреть, какая часть каждого из отношений закеширована и насколько активно используются эти данные (в этом запросе активными считаются страницы со счетчиком использования больше единицы):

<span id="internal-ru-page-205" class="pdf-anchor"></span>

```text {label="psql"}
=> SELECT c.relname,
  count(*) blocks,
  round( 100.0 * 8192 * count(*) /
    pg_table_size(c.oid) ) AS "% of rel",
  round( 100.0 * 8192 * count(*) FILTER (WHERE b.usagecount > 1) /
    pg_table_size(c.oid) ) AS "% hot"
FROM pg_buffercache b
  JOIN pg_class c ON pg_relation_filenode(c.oid) = b.relfilenode
WHERE b.reldatabase IN (
  0, -- общие объекты кластера
  (SELECT oid FROM pg_database
   WHERE datname = current_database())
)
AND b.usagecount IS NOT NULL
GROUP BY c.relname, c.oid
ORDER BY 2 DESC
LIMIT 5;
   relname    | blocks | % of rel | % hot
--------------+--------+----------+-------
 big          |   4097 |      100 |     2
 pg_attribute |     58 |       94 |    50
 pg_proc      |     24 |       20 |    15
 pg_class     |     17 |       94 |    67
 big_pkey     |     14 |      100 |   100
(5 rows)
```

Здесь видно, что таблица `big` и ее индекс полностью закешированы, но их страницы не используются активно.

Стоит учитывать, что запросы к `pg_buffercache`:

- надо повторять несколько раз, так как цифры будут меняться в определенных пределах;

- не надо выполнять непрерывно, потому что представление блокирует, хоть и кратковременно, просматриваемые буферы.

Поэтому для мониторинга лучше пользоваться другими функциями расширения — `pg_buffercache_summary` и `pg_buffercache_usage_counts`, — которые требуют меньше блокировок и за счет этого позволяют быстро оценить общую (хотя, возможно, немного рассогласованную) картину происходящего. (v. 16)

<span id="internal-ru-dummy-80" class="pdf-anchor"></span><span id="internal-ru-page-206" class="pdf-anchor"></span>

Во-вторых, подробная информация о вводе-выводе, проходящем через буферный кеш, накапливается в представлении `pg_stat_io`. Например, можно изучить количество операций ввода-вывода в разбивке по стратегиям использования буферных колец (контекст `normal` соответствует обычным операциям без кольца): (v. 16)

```text {label="psql"}
=> SELECT context,
  sum(reads) AS reads, sum(writes) AS writes,
  sum(hits) AS hits, sum(evictions) AS evictions, sum(reuses) AS reuses
FROM pg_stat_io
WHERE object = 'relation'
  AND context in ('normal','bulkread','bulkwrite','vacuum')
GROUP BY context;
  context  | reads  | writes |   hits   | evictions | reuses
-----------+--------+--------+----------+-----------+--------
 vacuum    | 138091 | 539140 |    89134 |      1846 | 656633
 normal    |  95057 | 342503 | 71883823 |    271326 |
 bulkwrite |      0 |      0 |        8 |         0 |      0
 bulkread  |  30506 | 155238 |    77778 |    175469 |  99285
(4 rows)
```

Запрос также показывает число попаданий в кеш (`hits`) и вытеснений из кеша: «обычные» вытеснения учитываются в поле `evictions`, а вытеснения страниц из буферного кольца — в поле `reuses`. Из этих цифр можно сделать определенные выводы о характере использования кеша.

Кроме количества операций чтения и записи (`reads` и `writes`), из представления можно узнать и объем ввода-вывода (`read_bytes` и `write_bytes`) в байтах: (v. 18)

<span id="internal-ru-page-207" class="pdf-anchor"></span>

```text {label="psql"}
=> SELECT context,
  pg_size_pretty(read_bytes) AS read_bytes,
  round(read_bytes/reads/8192, 1) AS pages_per_read,
  pg_size_pretty(write_bytes) AS write_bytes,
  round(write_bytes/writes/8192, 1) AS pages_per_write
FROM (
  SELECT context,
    sum(reads) AS reads,
    sum(read_bytes) AS read_bytes,
    sum(writes) AS writes,
    sum(write_bytes) AS write_bytes
  FROM pg_stat_io
  WHERE object = 'relation'
  GROUP BY context
  HAVING sum(reads) + sum(writes) > 0
);
 context  | read_bytes | pages_per_read | write_bytes | pages_per_write
----------+------------+----------------+-------------+-----------------
 vacuum   | 5147 MB    |            4.8 | 4212 MB     |             1.0
 normal   | 760 MB     |            1.0 | 2676 MB     |             1.0
 bulkread | 2155 MB    |            9.0 | 1213 MB     |             1.0
(3 rows)
```

Здесь мы видим, что очистка и последовательное сканирование читают сразу несколько страниц за одну операцию ввода-вывода, а вот запись пока всегда одностраничная.

При включенном параметре *track\_io\_timing* в этом же представлении отслеживается и время, затраченное на ввод-вывод. Однако, как я уже говорил, такое отслеживание может приводить к существенным накладным расходам на некоторых платформах. (*с.* [166](/ru/07-autovacuum/#internal-ru-page-167))

Представление `pg_stat_io` показывает данные, сгруппированные по типам процессов. Детализацию до конкретного обслуживающего процесса (пока он работает) можно получить функцией `pg_stat_get_backend_io`, которая возвращает таблицу с тем же набором полей. (v. 18)

Изучение данных `pg_buffercache` и `pg_stat_io` в различных разрезах может дать полезную информацию для размышлений.

## 9.7. Прогрев кеша {#internal-ru-section-9-7}

После перезапуска сервера должно пройти некоторое время, чтобы кеш «прогрелся» — набрал актуальные активно использующиеся данные. Иногда может оказаться полезным сразу прочитать в кеш данные определенных таблиц, и для этого предназначено расширение `pg_prewarm`:

```text {label="psql"}
=> CREATE EXTENSION pg_prewarm;
```

Кроме обычного чтения таблиц в буферный кеш (или только в кеш операционной системы), расширение позволяет сохранять актуальное состояние кеша на диск и восстанавливать его же после перезагрузки сервера. Для этого надо добавить библиотеку расширения в параметр *shared\_preload\_libraries* и перезагрузить сервер:

<span id="internal-ru-page-208" class="pdf-anchor"></span>

```text {label="psql"}
=> ALTER SYSTEM SET shared_preload_libraries = 'pg_prewarm';
postgres$ pg_ctl restart -l /home/postgres/logfile
```

После перезапуска, если значение параметра *pg\_prewarm.autoprewarm* не менялось, будет автоматически запущен фоновый процесс autoprewarm leader, который раз в *pg\_prewarm.autoprewarm\_interval* единиц времени будет сбрасывать на диск список страниц, находящихся в кеше (этот процесс использует один из слотов *max\_parallel\_processes*). (*по умолчанию:* `on`) (*по умолчанию:* `300s`)

> [!NOTE]
> Изначально процесс назывался autoprewarm master, но в версии 13 и он попал под раздачу.

```text
postgres$ ps -o pid,command \
--ppid `head -n 1 /usr/local/pgsql/data/postmaster.pid` | \
grep prewarm
  57518 postgres: autoprewarm leader
```

Сейчас, после перезагрузки, таблица `big` в кеше отсутствует:

```text {label="psql"}
=> SELECT count(*)
FROM pg_buffercache
WHERE relfilenode = pg_relation_filenode('big'::regclass);
 count
-------
     0
(1 row)
```

Если есть обоснованные предположения, что все содержимое таблицы будет активно использоваться, а доступ к диску недопустимо снизит время отклика запросов, таблицу можно заранее прочитать в буферный кеш:

```text {label="psql"}
=> SELECT pg_prewarm('big');
 pg_prewarm
------------
       4097
(1 row)
=> SELECT count(*)
FROM pg_buffercache
WHERE relfilenode = pg_relation_filenode('big'::regclass);
 count
-------
  4097
(1 row)
```

<span id="internal-ru-page-209" class="pdf-anchor"></span>

Список страниц сбрасывается в файл PGDATA/autoprewarm.blocks. Можно подождать, пока процесс autoprewarm leader отработает в первый раз, но мы инициируем запись вручную:

```text {label="psql"}
=> SELECT autoprewarm_dump_now();
 autoprewarm_dump_now
----------------------
                 4262
(1 row)
```

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

```text
postgres$ head -n 10 /usr/local/pgsql/data/autoprewarm.blocks
<<4262>>
0,1664,1262,0,0
0,1664,1260,0,0
16391,1663,1259,0,0
16391,1663,1259,0,1
16391,1663,1259,0,2
16391,1663,1259,0,3
16391,1663,1259,0,4
16391,1663,1259,0,5
16391,1663,1259,0,6
```

Теперь снова перезапустим сервер.

```bash
postgres$ pg_ctl restart -l /home/postgres/logfile
```

Таблица сразу же оказывается в кеше:

```text {label="psql"}
=> SELECT count(*)
FROM pg_buffercache
WHERE relfilenode = pg_relation_filenode('big'::regclass);
 count
-------
  4097
(1 row)
```

<span id="internal-ru-page-210" class="pdf-anchor"></span>

Это обеспечивается тем же самым процессом autoprewarm leader: он читает файл, разделяет страницы по базам данных, сортирует их (чтобы чтение с диска было по возможности последовательным) и передает отдельному рабочему процессу autoprewarm worker для обработки.

## 9.8. Локальный кеш {#internal-ru-section-9-8}

<span id="internal-ru-dummy-81" class="pdf-anchor"></span>

Исключением из общего правила являются временные таблицы. Поскольку временные данные видны только одному процессу, им нечего делать в общем буферном кеше. Поэтому для временных данных используется кеш в локальной памяти процесса, владеющего таблицей[^internal-ru-210-1].

В целом локальный буферный кеш устроен так же, как и общий:

- для поиска страниц используется собственная хеш-таблица;

- страницы вытесняются по обычному алгоритму (но без использования буферных колец);

- страницы могут закрепляться, чтобы избежать вытеснения.

Однако его реализация серьезно упрощается, поскольку отпадает необходимость в блокировках в оперативной памяти (так как доступ к буферам имеет только один процесс) и в защите данных от сбоя (так как данные в любом случае существуют максимум до конца сеанса). (*с.* [306](/ru/15-locks-on-memory-structures/#internal-ru-page-307)) (*с.* [211](/ru/10-write-ahead-log/#internal-ru-page-212))

Память под локальный кеш выделяется постепенно, по мере необходимости, поскольку временные таблицы используются далеко не во всех сеансах. Максимальный объем кеша одного сеанса ограничен параметром *temp\_buffers*. (*по умолчанию:* `8MB`)

> [!NOTE]
> Параметр с созвучным именем *temp\_file\_limit* относится не к временным таблицам, а к файлам, которые могут создаваться при выполнении запросов для временного хранения промежуточных данных.

В выводе команды `EXPLAIN` обращения к локальному буферному кешу отмечаются словом `local` вместо `shared`:

<span id="internal-ru-page-211" class="pdf-anchor"></span>

```text {label="psql" hl_lines="7"}
=> CREATE TEMPORARY TABLE tmp AS SELECT 1;
=> EXPLAIN (analyze, costs off, timing off, summary off)
  SELECT * FROM tmp;
                 QUERY PLAN
--------------------------------------------
 Seq Scan on tmp (actual rows=1.00 loops=1)
   Buffers: local hit=1
 Planning:
   Buffers: shared hit=12 read=7
(4 rows)
```

Данные о вводе-выводе страниц локальных кешей процессов позволяет получить уже знакомое нам представление `pg_stat_io`: (v. 16)

```text {label="psql" hl_lines="4"}
=> SELECT context, sum(reads) reads, sum(writes) writes,
  sum(hits) hits, sum(evictions) evictions, sum(reuses) reuses
FROM pg_stat_io
WHERE object = 'temp relation'
GROUP BY context;
 context | reads | writes | hits | evictions | reuses
---------+-------+--------+------+-----------+--------
 normal  |     0 |      0 |    1 |         0 |
(1 row)
```

[^internal-ru-186-1]: [backend/storage/aio/README.md](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/aio/README.md;hb=REL_18_STABLE>).
[^internal-ru-186-2]: [backend/storage/aio/method\_worker.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/aio/method_worker.c;hb=REL_18_STABLE>).
[^internal-ru-187-1]: [backend/storage/aio/method\_io\_uring.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/aio/method_io_uring.c;hb=REL_18_STABLE>).
[^internal-ru-188-1]: [backend/storage/aio/aio\_init.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/aio/aio_init.c;hb=REL_18_STABLE>), функция AioChooseMaxConcurrency.
[^internal-ru-189-1]: [backend/access/heap/heapam.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/heap/heapam.c;hb=REL_18_STABLE>), функция index\_delete\_prefetch\_buffer.
[^internal-ru-189-2]: [backend/access/transam/xlogprefetcher.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/transam/xlogprefetcher.c;hb=REL_18_STABLE>).
[^internal-ru-189-3]: [backend/storage/buffer/README](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/buffer/README;hb=REL_18_STABLE>).
[^internal-ru-189-4]: [include/storage/buf\_internals.h](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/include/storage/buf_internals.h;hb=REL_18_STABLE>).
[^internal-ru-190-1]: [backend/storage/buffer/bufmgr.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/buffer/bufmgr.c;hb=REL_18_STABLE>).
[^internal-ru-191-1]: [backend/storage/buffer/bufmgr.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/buffer/bufmgr.c;hb=REL_18_STABLE>), функция ReadBuffer\_common.
[^internal-ru-191-2]: [backend/storage/buffer/buf\_table.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/buffer/buf_table.c;hb=REL_18_STABLE>).
[^internal-ru-192-1]: [backend/utils/hash/dynahash.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/hash/dynahash.c;hb=REL_18_STABLE>).
[^internal-ru-192-2]: [backend/storage/buffer/bufmgr.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/buffer/bufmgr.c;hb=REL_18_STABLE>), функция DropRelFileNodeBuffers.
[^internal-ru-194-1]: [backend/storage/buffer/bufmgr.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/buffer/bufmgr.c;hb=REL_18_STABLE>), функция LockBufferForCleanup.
[^internal-ru-197-1]: [backend/storage/buffer/freelist.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/buffer/freelist.c;hb=REL_18_STABLE>), функция StrategyGetBuffer.
[^internal-ru-197-2]: [backend/storage/buffer/freelist.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/buffer/freelist.c;hb=REL_18_STABLE>), функция StrategyFreeBuffer.
[^internal-ru-199-1]: [backend/storage/buffer/freelist.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/buffer/freelist.c;hb=REL_18_STABLE>), функция GetBufferFromRing.
[^internal-ru-200-1]: [backend/storage/buffer/freelist.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/buffer/freelist.c;hb=REL_18_STABLE>), функция GetAccessStrategy.
[^internal-ru-200-2]: [backend/access/common/syncscan.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/common/syncscan.c;hb=REL_18_STABLE>).
[^internal-ru-210-1]: [backend/storage/buffer/localbuf.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/buffer/localbuf.c;hb=REL_18_STABLE>).
