# Занятие 08: Журналирование / Буферный кеш

> Исходная страница в нативном Markdown: Журналирование / Буферный кеш

---

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

---

<!-- Generated by scripts/generate_skeleton.py. -->

> [!NOTE]
> Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета.
> **HTML source:** [dba2_08_wal_buffercache.html](https://edu.postgrespro.ru/16/dba2-16/dba2_08_wal_buffercache.html)
> **Archive:** [DBA2-handouts-16.zip](https://edu.postgrespro.ru/16/DBA2-handouts-16.zip)
> **Страниц PDF:** 29

## Page 1: Журналирование / Буферный кеш {#source-page-001}

### Авторские права

© Postgres Professional, 2016–2025 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)
### Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.
### Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru
### Отказ от ответственности

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

## Page 2: Темы {#source-page-002}

- Устройство и использование буферного кеша
- Механизм вытеснения страниц
- Массовое вытеснение и буферные кольца
- Настройка размера кеша
- Локальный кеш для временных таблиц
- Прогрев кеша

## Page 3: Буферный кеш {#source-page-003}

![Source slide page 3: Буферный кеш](images/page-003.png){#source-figure-003 caption="Source slide page 3: Буферный кеш"}


### Комментарии преподавателя

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

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

● расположение страницы на диске (файл и номер страницы в нем);

● число обращений к буферу (счетчик usage count, увеличивается каждый раз, когда процесс читает или изменяет буфер);

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

● признак закрепления (счетчик pin count количества процессов, работающих в данный момент с содержимым буфера).

Размер буферного кеша задается параметром shared\_buffers. Его изменение требует перезапуска сервера.

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

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

## Page 4: Страница в кеше {#source-page-004}

![Source slide page 4: Страница в кеше](images/page-004.png){#source-figure-004 caption="Source slide page 4: Страница в кеше"}


### Комментарии преподавателя

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

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

Если нужная страница найдена в кеше, процесс должен «закрепить» буфер (увеличить счетчик pin count) и увеличить число обращений (счетчик usage count). Буфер могут закреплять несколько процессов одновременно, поэтому используется именно счетчик, а не логический признак.

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

## Page 5: Страница в кеше {#source-page-005}

```sql
=> CREATE DATABASE wal_buffercache;
```
```sql
CREATE DATABASE
```
```sql
=> \c wal_buffercache
```
```text
You are now connected to database "wal_buffercache" as user "student".
```
Создадим таблицу с одной строкой:
```sql
=> CREATE TABLE test(
  t text
)
WITH (autovacuum_enabled = off);
```
```sql
CREATE TABLE
```
```sql
=> INSERT INTO test VALUES ('a row');
```
```sql
INSERT 0 1
```
Информацию о содержимом буферного кеша и его использовании можно получить с помощью расширения pg\_buffercache, включающего в себя одноименное представление и дополнительные функции:
```sql
=> CREATE EXTENSION pg_buffercache;
```
```sql
CREATE EXTENSION
```
Создадим для удобства представление, расшифровывающее некоторые столбцы.
Условие на базу данных необходимо, так как в буферном кеше содержатся данные всего кластера. Расшифровать мы можем только информацию из той БД, к которой подключены. Глобальные объекты считаются принадлежащими БД с нулевым OID.
```sql
=> CREATE VIEW pg_buffercache_v AS
SELECT bufferid,
       (SELECT c.relname
        FROM   pg_class c
        WHERE  pg_relation_filenode(c.oid) = b.relfilenode
       ) relname,
       CASE relforknumber
         WHEN 0 THEN 'main'
         WHEN 1 THEN 'fsm'
         WHEN 2 THEN 'vm'
       END relfork,
       relblocknumber,
       isdirty,
       usagecount
FROM   pg_buffercache b
WHERE  b.reldatabase IN (
         0, (SELECT oid FROM pg_database WHERE datname = current_database())
       )
AND    b.usagecount IS NOT NULL;
```
```sql
CREATE VIEW
```
В буферном кеше уже находятся страницы таблицы; они появились при выполнении вставки:
```sql
=> SELECT * FROM pg_buffercache_v WHERE relname = 'test';
```
```text
 bufferid | relname | relfork | relblocknumber | isdirty | usagecount
----------+---------+---------+----------------+---------+------------
     1852 | test    | main    |              0 | t       |          1
(1 row)
```
Команда EXPLAIN с указанием analyze и buffers показывает использование буферного кеша при выполнении запроса:
```sql
=> EXPLAIN (analyze,buffers,costs off,timing off,summary off)
SELECT * FROM test;
```

## Page 6: Page 6 {#source-page-006}

```text
                QUERY PLAN
------------------------------------------
 Seq Scan on test (actual rows=1 loops=1)
   Buffers: shared hit=1
 Planning:
   Buffers: shared hit=4
(4 rows)
```
Строка «Buffers: shared hit=1» говорит о том, что страница была найдена в кеше.
После выполнения запроса счетчик использования увеличился на единицу:
```sql
=> SELECT * FROM pg_buffercache_v WHERE relname = 'test';
```
```text
 bufferid | relname | relfork | relblocknumber | isdirty | usagecount
----------+---------+---------+----------------+---------+------------
     1852 | test    | main    |              0 | t       |          2
(1 row)
```

## Page 7: Чтение в свободный буфер {#source-page-007}

![Source slide page 7: Чтение в свободный буфер](images/page-007.png){#source-figure-007 caption="Source slide page 7: Чтение в свободный буфер"}


### Комментарии преподавателя

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

В первую очередь поиск подходящего буфера будет проходить по списку свободных буферов, если он не пуст.

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

Указатель на свободный буфер передвигается на следующий по списку буфер, если он есть.

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

## Page 8: Чтение в свободный буфер {#source-page-008}

![Source slide page 8: Чтение в свободный буфер](images/page-008.png){#source-figure-008 caption="Source slide page 8: Чтение в свободный буфер"}


### Комментарии преподавателя

В конце концов будет заполнен и последний свободный буфер.

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

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

## Page 9: Чтение в свободный буфер {#source-page-009}

Перезагрузим сервер, чтобы сбросить буферный кеш.
```sh
student$ sudo pg_ctlcluster 16 main restart
```
```sh
student$ psql wal_buffercache
```
В кеше есть свободные буферы:
```sql
=> SELECT count(*) FROM pg_buffercache WHERE usagecount IS NULL;
```
```text
 count
-------
 16204
(1 row)
```
Выполним запрос к таблице:
```sql
=> EXPLAIN (analyze,buffers,costs off,timing off,summary off)
SELECT * FROM test;
```
```text
                QUERY PLAN
------------------------------------------
 Seq Scan on test (actual rows=1 loops=1)
   Buffers: shared read=1
 Planning:
   Buffers: shared hit=12 read=7
(4 rows)
```
Строка «Buffers: shared read=1» показывает, что страницу пришлось прочитать с диска. Вот она:
```sql
=> SELECT * FROM pg_buffercache_v WHERE relname = 'test';
```
```text
 bufferid | relname | relfork | relblocknumber | isdirty | usagecount
----------+---------+---------+----------------+---------+------------
      189 | test    | main    |              0 | f       |          1
(1 row)
```
Количество свободных буферов уменьшилось:
```sql
=> SELECT count(*) FROM pg_buffercache WHERE usagecount IS NULL;
```
```text
 count
-------
 16152
(1 row)
```
В кеш попала не только прочитанная страница, но и страницы таблиц системного каталога, которые потребовались планировщику.

## Page 10: Чтение с вытеснением {#source-page-010}

![Source slide page 10: Чтение с вытеснением](images/page-010.png){#source-figure-010 caption="Source slide page 10: Чтение с вытеснением"}


### Комментарии преподавателя

Механизм вытеснения использует указатель на следующую «жертву».

Алгоритм состоит в том, чтобы перебирать по кругу все буферы, уменьшая их счетчики обращений (usage count). Выбирается первый же буфер, у которого значение счетчика равно нулю и который в принципе может быть занят новой страницей. По-английски этот алгоритм называется clock-sweep: аналогия с часовой стрелкой, которая пробегает циферблат по кругу.

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

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

## Page 11: Чтение с вытеснением {#source-page-011}

![Source slide page 11: Чтение с вытеснением](images/page-011.png){#source-figure-011 caption="Source slide page 11: Чтение с вытеснением"}


### Комментарии преподавателя

Следующий буфер не закреплен, однако его счетчик обращений больше нуля.

Мы уменьшаем счетчик на единицу и даем странице шанс остаться в кеше.

## Page 12: Чтение с вытеснением {#source-page-012}

![Source slide page 12: Чтение с вытеснением](images/page-012.png){#source-figure-012 caption="Source slide page 12: Чтение с вытеснением"}


### Комментарии преподавателя

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

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

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

## Page 13: Чтение с вытеснением {#source-page-013}

![Source slide page 13: Чтение с вытеснением](images/page-013.png){#source-figure-013 caption="Source slide page 13: Чтение с вытеснением"}


### Комментарии преподавателя

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

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

## Page 14: Настройка размера кеша {#source-page-014}

**Настройка**
  - ↑
  - shared\_buffers = 128MB
- Буферный кеш должен содержать «активные» данные
  - при меньшем размере постоянно вытесняются полезные страницы
  - при большем размере бессмысленно растут накладные расходы
  - начальное приближение — 1/4 ОЗУ
- Нужно учитывать двойное кеширование
  - если страницы нет в кеше СУБД, она может оказаться в кеше ОС
  - алгоритм вытеснения ОС не учитывает специфики базы данных

### Комментарии преподавателя

Размер кеша устанавливается параметром shared\_buffers. Значение по умолчанию — 128 МБ — сильно занижено.

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

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

Однако при большом размере кеша будут расти накладные расходы на его поддержание.

В качестве первого приближения можно взять 1/4 оперативной памяти, но надо понимать, что оптимальное значение зависит не от размера ОЗУ, а от конкретных данных и нагрузки.

Не следует забывать и о том, что PostgreSQL работает с диском через операционную систему и, таким образом, происходит двойное кеширование: страницы попадают как в буферный кеш, так и в кеш ОС. Таким образом, «непопадание» в буферный кеш не всегда приводит к необходимости реального ввода-вывода. Но стратегия вытеснения ОС не учитывает специфики баз данных.

<https://postgrespro.ru/docs/postgresql/16/runtime-config-resource#RUNTIM> E-CONFIG-RESOURCE-MEMORY

## Page 15: Настройка размера кеша {#source-page-015}

Используя расширение pg\_buffercache, можно наблюдать за состоянием кеша под разными углами.
Кроме информации о том, как представлена в кеше та или иная страница, можно, например, посмотреть распределение буферов по их «популярности».
```sql
=> SELECT usage_count, buffers AS count FROM pg_buffercache_usage_counts();
```
```text
 usage_count | count
-------------+-------
           0 | 16150
           1 |    46
           2 |    34
           3 |    18
           4 |     5
           5 |   131
(6 rows)
```
Функция pg\_buffercache\_usage\_counts возвращает количество общих буферов для всех возможных значений счетчика использования.
Как и представление pg\_buffercache, функция не использует блокировки менеджера буферов, поэтому при параллельной работе в базе данных возможна незначительная погрешность результатов функции.
Посмотрим, какая доля каких таблиц закеширована (и насколько активно используются эти данные):
```sql
=> SELECT c.relname,
  count(*) blocks,
  round( 100.0 * 8192 * count(*) / pg_table_size(c.oid) ) "% of rel",
  round( 100.0 * 8192 * count(*) FILTER (WHERE b.usagecount > 3) / pg_table_size(c.oid) ) "% 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 10;
```
```text
             relname             | blocks | % of rel | % hot
---------------------------------+--------+----------+-------
 pg_attribute                    |     31 |       50 |    47
 pg_proc                         |     14 |       13 |     2
 pg_class                        |     14 |       78 |    78
 pg_operator                     |     11 |       61 |    39
 pg_proc_oid_index               |      9 |       82 |    27
 pg_attribute_relid_attnum_index |      8 |       73 |    55
 pg_proc_proname_args_nsp_index  |      7 |       22 |     3
 pg_amproc                       |      5 |       56 |    11
 pg_amop                         |      5 |       45 |    27
 pg_operator_oprname_l_r_n_index |      5 |       83 |    33
(10 rows)
```
А сводную информацию о буферном кеше нам покажет другая функция из расширения:
```sql
=> SELECT * FROM pg_buffercache_summary() \gx
```
```text
-[ RECORD 1 ]--+------------------
buffers_used   | 265
buffers_unused | 16119
buffers_dirty  | 6
buffers_pinned | 0
usagecount_avg | 3.709433962264151
```
Подобные запросы могут подсказать, насколько активно используется буферный кеш и дать пищу для размышлений о том, стоит ли увеличивать или уменьшать его размер. Надо учитывать, что такие запросы надо повторять несколько раз: цифры будут меняться в определенных пределах, а набор результатов, полученный для всех буферов, в целом может оказаться несогласованным.
Информацию, подобную той, что возвращают функции pg\_buffercache\_summary() и pg\_buffercache\_usage\_counts(), можно получить и с помощью представления pg\_buffercache, однако использование функций менее затратно, что удобно для использования в системах мониторинга.

## Page 16: Page 16 {#source-page-016}


## Page 17: Массовое вытеснение {#source-page-017}

![Source slide page 17: Массовое вытеснение](images/page-017.png){#source-figure-017 caption="Source slide page 17: Массовое вытеснение"}


### Комментарии преподавателя

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

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

## Page 18: Буферное кольцо {#source-page-018}

**Последовательное чтение**
  - 32 страницы
  - грязный буфер исключается из кольца
- Очистка
  - vacuum\_buffer\_usage\_limit = 32
  - грязный буфер вытесняется на диск
- Массовая запись
  - ≤2048 страниц
  - грязный буфер вытесняется на диск

### Комментарии преподавателя

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

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

Для работы очистки используется кольцо, размер которого задается параметром vacuum\_buffer\_usage\_limit. По умолчанию его значение невелико (32 страницы), так как замедление очистки обычно не столь критично, как замедление пользовательских процессов (исключение — защитный режим: в нем очистка может использовать весь кеш).

Для массовых операций записи, таких как COPY или CREATE TABLE AS SELECT, кольцо имеет достаточно большой размер (обычно 2048 страниц, но не больше одной восьмой всего буферного кеша).

## Page 19: Массовое вытеснение {#source-page-019}

Для полного чтения таблиц, размер которых превышает четверть буферного кеша, используется буферное кольцо. Размер кеша (в страницах):
```sql
=> SELECT setting FROM pg_settings WHERE name='shared_buffers';
```
```text
 setting
---------
 16384
(1 row)
```
Добавим строк в таблицу:
```sql
=> INSERT INTO test SELECT repeat('A',1000) FROM generate_series(1,30_000);
```
```sql
INSERT 0 30000
```
```text
=> ANALYZE test;
```
```text
ANALYZE
```
```sql
=> SELECT relpages FROM pg_class WHERE relname = 'test';
```
```text
 relpages
----------
     4286
(1 row)
```
Перезагрузим сервер, чтобы сбросить буферный кеш.
```sh
student$ sudo pg_ctlcluster 16 main restart
```
```sh
student$ psql wal_buffercache
```
И сбросим общую статистику ввода-вывода:
```sql
=> SELECT pg_stat_reset_shared('io');
```
```text
 pg_stat_reset_shared
----------------------
(1 row)
```
Прочитаем данные из таблицы:
```sql
=> EXPLAIN (analyze,buffers,costs off,timing off,summary off)
SELECT count(*) FROM test;
```
```text
                     QUERY PLAN
----------------------------------------------------
 Aggregate (actual rows=1 loops=1)
   Buffers: shared read=4286
   ->  Seq Scan on test (actual rows=30001 loops=1)
         Buffers: shared read=4286
 Planning:
   Buffers: shared hit=14 read=8
(6 rows)
```
Были прочитаны все страницы с данными, но сколько буферов кеша ими занято?
```sql
=> SELECT count(*) FROM pg_buffercache_v WHERE relname = 'test';
```
```text
 count
-------
    32
(1 row)
```
При сканировании большой таблицы размер кольца — 32 буфера.
В 16-й версии PostgreSQL появилось представление pg\_stat\_io, которое показывает статистику ввода/вывода с точки зрения СУБД (кеширование на уровне ОС в нем не учитывается).

## Page 20: Page 20 {#source-page-020}

```sql
=> SELECT context, reads, hits, reuses FROM pg_stat_io
```
```sql
WHERE backend_type = 'client backend' AND
      object = 'relation' AND
      context IN ('normal', 'bulkread');
```
```text
 context  | reads | hits | reuses
----------+-------+------+--------
 bulkread |  4286 |    0 |   4254
 normal   |    92 | 4211 |
(2 rows)
```
Строка с контекстом normal показывает операции чтения-записи в общей части буферного кеша, а bulkread — в буферных кольцах.
reads — количество чтений из файлов, hits — сколько раз страница нашлась в кеше, reuses — число повторных использований буферов в буферном кольце.
Разность между reads и reuses в строке bulkread равна размеру кольца (32).
А теперь сбросим статистику и изменим все строки таблицы. При этом буферы со страницами будут отсоединяться от буферного кольца и оставаться в кеше:
```sql
=> SELECT pg_stat_reset_shared('io');
```
```text
 pg_stat_reset_shared
----------------------
(1 row)
```
```sql
=> EXPLAIN (analyze,buffers,costs off,timing off,summary off)
UPDATE test SET t = t || '!';
```
```sql
                           QUERY PLAN
-----------------------------------------------------------------
 Update on test (actual rows=0 loops=1)
   Buffers: shared hit=98604 read=4256 dirtied=8574 written=4307
   ->  Seq Scan on test (actual rows=30001 loops=1)
         Buffers: shared hit=32 read=4254 written=20
 Planning:
   Buffers: shared hit=4 read=2 dirtied=1
(6 rows)
```
Сейчас в кеше находятся все или почти все страницы таблицы, количество которых удвоилось из-за появления новых версий строк:
```sql
=> SELECT relfork, count(*) FROM pg_buffercache_v WHERE relname = 'test' GROUP BY relfork;
```
```text
 relfork | count
---------+-------
 fsm     |     3
 main    |  8552
(2 rows)
```
Еще раз заглянем в статистику.
```sql
=> SELECT context, reads, hits, reuses FROM pg_stat_io
WHERE backend_type = 'client backend' AND
      object = 'relation' AND
      context IN ('normal', 'bulkread');
```
```text
 context  | reads |  hits  | reuses
----------+-------+--------+--------
 bulkread |  4254 |     32 |     20
 normal   |    10 | 220987 |
(2 rows)
```
Таблица еще раз полностью сканируется через буферное кольцо. При этом 32 страницы остались в кольце от предыдущего сканирования, а остальные пришлось прочитать заново. Измененные страницы одна за другой передаются в общий кеш, обращения к ним учитываются в строке normal. При этом обновление каждой из 30 тысяч строк таблицы требует нескольких обращений к страницам, поэтому значение в поле hits довольно велико.

## Page 21: Локальный кеш {#source-page-021}

**Временные таблицы**
  - видны только одному сеансу — нет смысла использовать общий кеш
  - существуют в пределах сеанса — не жалко потерять при сбое
- Особенности
  - не требуются блокировки
  - память выделяется по необходимости в пределах temp\_buffers
  - обычный алгоритм вытеснения
  - механизм буферных колец не используется

### Комментарии преподавателя

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

Для временных данных используется облегченный локальный кеш.

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

Временные таблицы на физическом уровне хранятся схожим с обычными таблицами образом. Параметр temp\_tablespaces задает табличные пространства, в которых будут создаваться временные объекты (временные таблицы и индексы временных таблиц), когда в команде CREATE табличное пространство не указывается явно.

Значение параметра temp\_tablespaces может содержать список имен табличных пространств. Когда этот список содержит больше одного имени, PostgreSQL выбирает из него случайный элемент при создании каждого временного объекта; однако при создании последующих объектов внутри транзакции табличные пространства перебираются последовательно. Если же в списке оказывается пустая строка (значение по умолчанию), PostgreSQL будет использовать табличное пространство по умолчанию для текущей базы данных.

## Page 22: Временные таблицы {#source-page-022}

Создадим временную таблицу с одной строкой:
```sql
=> CREATE TEMP TABLE test_tmp(
  t text
);
```
```sql
CREATE TABLE
```
```sql
=> INSERT INTO test_tmp VALUES ('a row');
```
```sql
INSERT 0 1
```
В плане выполнения запроса обращение к локальному кешу выглядит как «Buffers: local»:
```sql
=> EXPLAIN (analyze,buffers,costs off,timing off,summary off)
SELECT * FROM test_tmp;
```
```text
                  QUERY PLAN
----------------------------------------------
 Seq Scan on test_tmp (actual rows=1 loops=1)
   Buffers: local hit=1
 Planning:
   Buffers: shared hit=4
(4 rows)
```
```sql
=> SELECT context, hits, extends FROM pg_stat_io
WHERE backend_type = 'client backend' AND
      object = 'temp relation' AND context = 'normal';
```
```text
 context | hits | extends
---------+------+---------
 normal  |    1 |       1
(1 row)
```
Видим, что вставка строки привела к расширению таблицы (extends).

## Page 23: Прогрев кеша {#source-page-023}

**Расширение pg\\\_prewarm**
  - ручной прогрев кеша операционной системы
  - ручной прогрев буферного кеша
  - автоматический прогрев буферного кеша при перезапуске сервера

### Комментарии преподавателя

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

Расширение pg\_prewarm позволяет в ручном режиме прочитать некоторые таблицы либо в кеш операционной системы, либо и в буферный кеш СУБД тоже.

Расширение также позволяет в автоматическом режиме восстановить содержимое буферного кеша после перезапуска сервера. Для этого необходимо подключить библиотеку расширения, добавив ее в параметр shared\_preload\_libraries.

<https://postgrespro.ru/docs/postgresql/16/pgprewarm>

## Page 24: Прогрев кеша {#source-page-024}

Рассмотрим самый простой сценарий использования расширения для прогрева кеша.
```sql
=> CREATE EXTENSION pg_prewarm;
```
```sql
CREATE EXTENSION
```
В очередной раз перезапустим сервер, чтобы в кеше не было данных таблицы test:
```sh
student$ sudo pg_ctlcluster 16 main restart
```
```sh
student$ psql wal_buffercache
```
```sql
=> SELECT count(*) FROM pg_buffercache_v WHERE relname = 'test';
```
```text
 count
-------
     0
(1 row)
```
Вызов функции pg\_prewarm без дополнительных параметров полностью считывает основной слой указанной таблицы в буферный кеш:
```sql
=> SELECT pg_prewarm('test');
```
```text
 pg_prewarm
------------
       8572
(1 row)
```
```sql
=> SELECT relfork, count(*) FROM pg_buffercache_v WHERE relname = 'test' GROUP BY relfork;
```
```text
 relfork | count
---------+-------
 main    |  8572
(1 row)
```

## Page 25: Итоги {#source-page-025}

**Вся работа с данными происходит через буферный кеш**
- Редко используемые страницы вытесняются, часто используемые — остаются
- Буферный кеш сокращает объем ввода-вывода, но приводит к необходимости журналирования

## Page 26: Практика {#source-page-026}

1. Создайте таблицу и вставьте в нее некоторое количество строк. Определите, сколько занимает таблица а) страниц на диске, б) буферов в кеше.
- 2. Узнайте количество грязных буферов в кеше на текущий момент. Выполните контрольную точку командой CHECKPOINT. Сколько грязных буферов осталось теперь?
- 3. Подключите библиотеку расширения pg\_prewarm и проверьте, что после перезапуска сервера содержимое буферного кеша восстанавливается.

### Комментарии преподавателя

Чтобы проверить содержимое буферного кеша, используйте расширение pg\_buffercache: <https://postgrespro.ru/docs/postgresql/16/pgbuffercache.html>

## Page 27: 1. Таблица в кеше {#source-page-027}

Создадим таблицу:
```sql
=> CREATE DATABASE wal_buffercache;
```
```sql
CREATE DATABASE
```
```sql
=> \c wal_buffercache
```
```text
You are now connected to database "wal_buffercache" as user "student".
```
```sql
=> CREATE TABLE t(n integer);
```
```sql
CREATE TABLE
```
```sql
=> INSERT INTO t SELECT 1 FROM generate_series(1,10_000);
```
```sql
INSERT 0 10000
```
Выполним VACUUM — это обеспечит нам наличие всех слоев:
```sql
=> VACUUM t;
```
```sql
VACUUM
```
Сколько страниц на диске занимает таблица?
```sql
=> SELECT setting FROM pg_settings WHERE name = 'block_size';
```
```text
 setting
---------
 8192
(1 row)
```
```sql
=> SELECT pg_table_size('t') / 8192;
```
```text
 ?column?
----------
       49
(1 row)
```
Из них основной слой:
```sql
=> SELECT pg_relation_size('t','main') / 8192;
```
```text
 ?column?
----------
       45
(1 row)
```
Карта свободного пространства:
```sql
=> SELECT pg_relation_size('t','fsm') / 8192;
```
```text
 ?column?
----------
        3
(1 row)
```
И карта видимости:
```sql
=> SELECT pg_relation_size('t','vm') / 8192;
```
```text
 ?column?
----------
        1
(1 row)
```
Сколько буферов в кеше занимает таблица?
```sql
=> CREATE EXTENSION pg_buffercache;
```
```sql
CREATE EXTENSION
```

## Page 28: Page 28 {#source-page-028}

```sql
=> SELECT CASE relforknumber
         WHEN 0 THEN 'main'
         WHEN 1 THEN 'fsm'
         WHEN 2 THEN 'vm'
       END relfork,
       count(*)
FROM   pg_buffercache b,
       pg_class c
WHERE  b.reldatabase = (
         SELECT oid FROM pg_database WHERE datname = current_database()
       )
AND    c.oid = 't'::regclass
AND    b.relfilenode = c.relfilenode
GROUP BY 1;
```
```text
 relfork | count
---------+-------
 fsm     |     3
 main    |    45
 vm      |     1
(3 rows)
```
### 2. Грязные буферы в кеше
```sql
=> SELECT buffers_dirty FROM pg_buffercache_summary();
```
```text
 buffers_dirty
---------------
           985
(1 row)
```
Выполним контрольную точку:
```text
=> CHECKPOINT;
```
```text
CHECKPOINT
```
```sql
=> SELECT buffers_dirty FROM pg_buffercache_summary();
```
```text
 buffers_dirty
---------------
             0
(1 row)
```
Грязных буферов не осталось. Подробнее о контрольной точке рассказывается в отдельной теме.
### 3. Автоматический прогрев кеша
Подключим библиотеку pg\_prewarm и перезапустим сервер.
```sql
=> ALTER SYSTEM SET shared_preload_libraries = 'pg_prewarm';
```
```sql
ALTER SYSTEM
```
```sh
student$ sudo pg_ctlcluster 16 main restart
```
```sh
student$ psql wal_buffercache
```
Теперь отдельный фоновый процесс будет сбрасывать на диск список страниц, находящихся в буферном кеше, раз в pg\_prewarm.autoprewarm\_interval единиц времени.
```sh
student$ ps -o pid,command --ppid $(sudo head -n 1 /var/lib/postgresql/16/main/postmaster.pid) | grep
prewarm
```
```text
  54839 postgres: 16/main: autoprewarm leader
```
Прочитаем таблицу t в буферный кеш:
```sql
=> CREATE EXTENSION pg_prewarm;
```
```sql
CREATE EXTENSION
```
```sql
=> SELECT pg_prewarm('t');
```
```text
 pg_prewarm
------------
         45
(1 row)
```

## Page 29: Page 29 {#source-page-029}

```sql
=> SELECT count(*)
FROM pg_buffercache
WHERE relfilenode = pg_relation_filenode('t'::regclass);
```
```text
 count
-------
    45
(1 row)
```
Можно либо подождать, либо сбросить список страниц вручную:
```sql
=> SELECT autoprewarm_dump_now();
```
```text
 autoprewarm_dump_now
----------------------
                  299
(1 row)
```
В файл записываются идентификаторы базы данных, табличного пространства и файла, номер слоя и номер страницы:
```sh
student$ sudo head -n 10 /var/lib/postgresql/16/main/autoprewarm.blocks
```
```text
<<299>>
0,1664,1262,0,0
0,1664,1260,0,0
1,1663,1259,0,0
1,1663,1259,0,1
1,1663,1259,0,2
1,1663,1259,0,3
1,1663,1249,0,0
1,1663,1249,0,1
1,1663,1249,0,2
```
Снова перезапустим сервер.
```sh
student$ sudo pg_ctlcluster 16 main restart
```
```sh
student$ psql wal_buffercache
```
После запуска фоновый процесс прочитает в буферный кеш все страницы, указанные в файле.
```sql
=> SELECT count(*)
FROM pg_buffercache
WHERE relfilenode = pg_relation_filenode('t'::regclass);
```
```text
 count
-------
    45
(1 row)
```
Вернемся к настройкам по умолчанию.
```sql
=> ALTER SYSTEM RESET shared_preload_libraries;
```
```sql
ALTER SYSTEM
```
```sh
student$ sudo pg_ctlcluster 16 main restart
```
