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

У каждого есть своя граница, где Postgres ещё применим, а дальше —уже нет. Обычно это где-то между одним и пятью терабайтами, дальше жить с этим «больно».

База просто не может обработать большой объем данных с той скоростью, которую способны выдать диски.

И вот — Postgres 18, впервые за долгое время, предлагает не косметическую, а фундаментальную новинку. То, что в Oracle есть уже 20+ лет — асинхронный ввод-вывод (аsync IO).

Попробуем посмотреть async IO и ответить на вопрос - стал ли Postgres ближе к «взрослым» нагрузкам?

Предыдущее нововведение такого масштаба было в Postgres 10 — нативное партиционирование. Оно позволило наконец-то разбивать данные на части и работать с ними полноценно, без триггеров и вьюх, которыми приходилось имитировать эту возм��жность раньше. На минуточку — это был 2017 год.

Пощупаем async I/O руками, не полагаясь на релиз-ноты и маркетинг.

Не на абстрактных тестах, где база читает бессмысленные страницы, а в сценариях, близких к реальной работе. Посмотрим, как ведёт себя новый ввод-вывод под типичными нагрузками разных классов систем: OLTP, OLAP, общего назначения.

О чем вообще идет речь.

В обычном режиме база не может обрабатывать данные с той скоростью, с которой их способны отдать диски.

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

И всё это делается последовательно.

Итого, в схеме «почитали-подумали» база данных успевает обработать поток всего 200–300 МБ/с. Для сравнения — даже обычный потребительский NVMe-диск легко отдаёт 3–6 ГБ/с, а серверная СХД — 10 ГБ/с и выше, с сотнями тысяч IOPS.

Одно из очевидных решений чтобы «выжать» из дисков побольше — параллелизм. Postgres давно умеет исполнять запросы в несколько потоков, читая данные одновременно и собирая общий результат. На практике это действительно ускоряет тяжёлые запросы, но не всегда.

Не всё можно делать параллельно
Не всё можно делать параллельно

Далеко не каждую операцию можно распараллелить, а при небольших объёмах накладные расходы легко «съедают» весь выигрыш.

И тут может помочь другое решение: что, если не ждать, пока диск прочитает данные, а начать обрабатывать предыдущую порцию, пока следующая всё ещё подгружается? Именно это и делает асинхронный ввод-вывод (async I/O).

Ораклистам хорошо знаком тот самый «волшебный» параметр — FILESYSTEMIO_OPTIONS=SETALL. Одна строка в конфиге включает сразу и асинхронный, и прямой ввод-вывод. Результат — не проценты, а кратный прирост скорости.

А если это так просто и эффективно, почему же остальные СУБД не сделали то же самое?

Дьявол в нюансах. Чтобы отправить запрос на чтение заранее, база должна знать, что именно ей понадобится дальше. Она не может просто читать «что попало». И вот здесь всё упирается не в сам async I/O, а в алгоритмы предсказания и планирования ввода-вывода — что читать наперёд и когда именно.

Посмотрим, насколько хорошо с этим справился Postgres 18.

Первым делом идем в документацию и смотрим, что нам предлагает 18-ый.

Что сразу бросается в глаза: async I/O работает только для чтения, но не для записи.

На первый взгляд это серьезное ограничение, но на самом деле — вовсе нет.
Даже «транзакционная» база, которая всё время что-то пишет, не работает просто «на запись». Чтобы что-то записать, нужно сначала прочитать то место, куда писать. А в обычной жизни клиентский процесс вообще не пишет напрямую — этим занимаются системные процессы walwriter и bgwriter.

Так что то, что асинхронность пока работает только для чтения — логичный шаг.

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

  • Sequential Scan — это, по сути, тот же Full Table Scan из Oracle: полное последовательное чтение таблицы, без участия индексов. Операция, которую стараются избегать в любых БД. Разве что при соединении со справочниками.

  • Bitmap Heap Scan — прямого аналога в Oracle нет; скорее, это похоже на чтение крупных фрагментов индекса, построение по ним битовой карты и последующее чтение таблицы уже по этой карте. Для выборки больших объемов данных в специфичных условиях.

  • VACUUM — расшифровывать, думаю, не нужно. Разве что для ораклиста сама идея регулярного «перетряхивания» данных звучит немного дико :) Чисто обслуживающая задача.

Чего мы не увидели в документации

Пока список поддерживаемых операций Async I/O выглядит скромно. И сразу бросается в глаза то, чего там нет.

  • Index Scan — в терминах Oracle это Index Range Scan + Table Access by ROWID. Пожалуй, самая распространённая операция почти в любой базе — легко занимает до 90% всех обращений к данным.

  • Hash Join — формально не относится напрямую к вводу-выводу, но объёмные соединения легко вываливаются на диск. Для крупных аналитических запросов это один из основных источников I/O-нагрузки.

Режимы async I/O

Документация уточняет, что режимов асинхронного чтения в Postgres 18 не один, а два: worker и io_uring. Плюс, осталась возможность вернуться в «классический» режим – sync.

Начнём с worker — именно он включён в Postgres 18 по умолчанию.

В режиме worker асинхронным чтением занимаются специально выделенные фоновые процессы Postgres. Их число задаётся в конфигурации и остаётся фиксированным на протяжении всей жизни экземпляра.

Иными словами, когда пользовательский процесс хочет что-то прочитать, он не лезет на диск сам — он ставит задачу другому процессу, а один из воркеров читает нужные блоки и возвращает результат. В итоге схема работы async I/O в этом режиме выглядит примерно так:

Возн��кает закономерный вопрос: не станут ли сами эти воркеры узким местом?

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

Второй режим — io_uring. Здесь асинхронным чтением занимается уже ядро операционной системы, а Postgres лишь формирует очередь запросов и получает уведомления о готовности.

Но и здесь есть нюанс — не все ядра Linux поддерживают io_uring. Если система достаточно старая, без обновления OS не обойтись.

Ну а теперь - результаты.

Проверим как ведёт себя новый ввод-вывод под типичными нагрузками разных классов систем:

  • транзакционных: OLTP (биллинговых и процессинговых),

  • прикладных систем общего назначения.

  • аналитических и Data Warehouse: OLAP/DWH

Транзакционные: OLTP

Под OLTP-нагрузкой понимаются характерные сценарии биллинга, процессинга, авторизации и тому подобные — где база данных отвечает на тысячи коротких запросов в секунду: «найди клиента», «проверь баланс», «спиши сумму», «запиши транзакцию». (Index Unique → Table Access)

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

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

Именно это и проверяем в тесте — выбираем одну строчку по ID и постепенно увеличиваем число параллельных сессий, пока сервер не войдёт в насыщение.

Ожидалось, что при OLTP-нагрузке результаты будут примерно одинаковыми для всех методов ввода-вывода во всём диапазоне нагрузок. Однако замеры показали небольшое, но стабильное отклонение.

На графике видно, что «дефолтовый» worker async I/O не даёт преимуществ в OLTP — даже немного снижает производительность. Сказываются накладные расходы на обмен между клиентским процессом и IO-воркерами.

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

Системы общего назначения.

К этому классу относятся почти все «повседневные» прикладные задачи: бухгалтерия, склад, учёт, ERP-модули, help-desk, интернет-магазины и т.д. В них нет экстремальной транзакционной нагрузки, как в процессинге, и нет тяжёлых аналитических запросов, как в OLAP/DWH.

Основная нагрузка — это постоянные выборки по индексам (Index Range → Table Access), часто продолжающиеся Join’ом или Sort’ом. Именно такие операции составляют до 90% работы типичной базы: поиск записей, вывод списков, уточнение деталей по идентификатору.

В тесте имитируем этот сценарий — выбираем по диапазону идентификаторов 10 тыс. строк и постепенно повышаем нагрузку до точки насыщения.

Ожидаемо, async I/O здесь не даёт заметного эффекта - эти операции не входят в список поддерживаемых. Но хуже не стало – уже хорошо, производительность остаётся на уровне классического ввода-вывода.

Условный OLAP/DWH.

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

Но давайте честно: это ещё не аналитика.

Реальная аналитика — это не суммирование по миллиарду строк, а кубы, сложные агрегации и тяжёлые Hash Join’ы.

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

А вот этого в Postgres пока нет — точнее, ещё не подвезли в рамках async I/O.

Поэтому проверяем то, что пока есть: полное сканирование большой таблицы и измерение времени - Full Scan.

Чем меньше время — тем лучше.

Здесь результат лучше, но, прямо сказать, не принципиально. Режим worker немного быстрее, io_uring никакой разницы с «классикой» не показал.

В реальной жизни полные сканирования встречаются нередко, но с важной оговоркой: это, как правило, соединения с небольшими справочниками, которые держатся в памяти и почти не создают дискового I/O.

Из любопытства протестировали и Hash Join частей таблиц.

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

Bitmap Heap Scan

Формально поддерживается в Postgres 18 для асинхронного ввода-вывода. Но чтобы оптимизатор выбрал этот вариант, должны совпасть достаточно узкие условия - когда для index scan данных уже много, а для full scan еще мало.

На практике такие ситуации встречаются редко, поэтому создадим искусственный пример: читаем в одном запросе несколько диапазонов по 100 тыс. строк и измеряем время — чем меньше, тем лучше.

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

Возможно, есть сценарии, где async I/O даст выигрыш, но это явно не массовая история.

Финализируя

В некоторых сценариях асинхронный ввод-вывод в Postgres 18 действительно даёт прирост, но небольшой — единицы процентов.

Стал ли Postgres ближе к «взрослым» нагрузкам?

На мой взгляд, пока нет. Для себя я ответ получил.

Может быть, и вы тоже.

Зато теперь вы знаете, какие правильные вопросы задать если к вам придут с заявлениями вроде: «Теперь-то Postgres не хуже Oracle!»

– «А при каких условиях и нагрузках?»
– «Вы сможете это показать?»

В любом случае, начало положено, будем следить за развитием.

Алексей Перегудов
Data Solutions Architect
Oracle Master
Postgres Expert
dba@dbtime.ru

Комментарии (9)


  1. VladimirFarshatov
    20.10.2025 15:55

    Что-то подобное подозревалось после опробывания, замеров не делал, на вскидку не заметил. )


  1. DSolodukhin
    20.10.2025 15:55

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


    1. AlekseyPeregudov Автор
      20.10.2025 15:55

      Тестировалось на разном железе.

      SATA SSD, три типа PCI-E NVME, и даже RAM-disk.

      Виртуализация и bare-metal от 1-го до 16-ти ядер

      С разными размерами баз, настройками буферов и степенью параллелизма.

      Меняются абсолютные показатели, но относительные всегда плюс/минус одинаковые.


  1. Sleuthhound
    20.10.2025 15:55

    Странная статья.

    Что такое взрослые нагрузки для Вас? Вы бы хоть в каком-то выражении их описали.

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

    Я тут мимо проходил, что-то написал, каких-то графиков накидал. Зачем?


  1. shurutov
    20.10.2025 15:55

    Предыдущее нововведение такого масштаба было в Postgres 10 — нативное партиционирование.

    Какое-то странное заявление. Нативное партиционирование без автоматического создания партиций - такое себе. Грустно, на самом деле, от такого партиционирования.
    С другой стороны партиционирование - это про рукоблудие по-взрослому.
    А параллельное сканирование, агрегаты и вот это вот всё, что появилось в 9.6 - это так, мелочи? Никак на производительность не влияют?


  1. vitaly_il1
    20.10.2025 15:55

    Если я ничего не упускаю, все графики/тесты на Postgres 18. ИМХО имеет смысл сравнить производительность с Postgres 17 на том же железе.


    1. AlekseyPeregudov Автор
      20.10.2025 15:55

      Абсолютно верное замечание.

      И первой была серия тестов версий 17 и 18 на том же самом железе, чтобы убедиться что 17-ый идентичен 18-му в "классическом" режиме.


  1. Antharas
    20.10.2025 15:55

    так, стоп.. с чего резко такая уверенность, что 1 и тот же запрос на разном сетевом стеке должен выполнятся быстрее или медленнее? 0_о


  1. VladimirFarshatov
    20.10.2025 15:55

    Мне было бы гораздо интереснее и любопытнее понять почему тяжелый запрос, выбирающий в одной сессии каждую десятую запись из набора (по условиям) и выполняющийся скажем 4секунды, в соседней сессии, выбирающий тоже каждую десятую запись (со смещением n % m) из того же набора выполняется .. а те же самые 4секунды. КЭШа в Постгрес не завезли? Выборка набора идет строго по одному комплекту условий?

    Решил ускорить сервис: распараллелил запросы асинхронно .. ан нет. 1 поток выбирает 4сек. и 8 потоков выбирают 35сек.. фантастика. Постгрес 14,15,17.