
Давайте честно – пока что 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)

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

AlekseyPeregudov Автор
20.10.2025 15:55Тестировалось на разном железе.
SATA SSD, три типа PCI-E NVME, и даже RAM-disk.
Виртуализация и bare-metal от 1-го до 16-ти ядер
С разными размерами баз, настройками буферов и степенью параллелизма.
Меняются абсолютные показатели, но относительные всегда плюс/минус одинаковые.

Sleuthhound
20.10.2025 15:55Странная статья.
Что такое взрослые нагрузки для Вас? Вы бы хоть в каком-то выражении их описали.
Если Вы хотите показать результаты бенчмарков, то выкладывайте и конфигурацию железа и конфигурацию пг и как проводили тесты. Хотите сравнить с ораклом - ну вперед, все те же данные нужны (железо, конфиги, тесты). Здесь же ничего.
Я тут мимо проходил, что-то написал, каких-то графиков накидал. Зачем?

shurutov
20.10.2025 15:55Предыдущее нововведение такого масштаба было в Postgres 10 — нативное партиционирование.
Какое-то странное заявление. Нативное партиционирование без автоматического создания партиций - такое себе. Грустно, на самом деле, от такого партиционирования.
С другой стороны партиционирование - это про рукоблудие по-взрослому.
А параллельное сканирование, агрегаты и вот это вот всё, что появилось в 9.6 - это так, мелочи? Никак на производительность не влияют?

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

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

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

VladimirFarshatov
20.10.2025 15:55Мне было бы гораздо интереснее и любопытнее понять почему тяжелый запрос, выбирающий в одной сессии каждую десятую запись из набора (по условиям) и выполняющийся скажем 4секунды, в соседней сессии, выбирающий тоже каждую десятую запись (со смещением n % m) из того же набора выполняется .. а те же самые 4секунды. КЭШа в Постгрес не завезли? Выборка набора идет строго по одному комплекту условий?
Решил ускорить сервис: распараллелил запросы асинхронно .. ан нет. 1 поток выбирает 4сек. и 8 потоков выбирают 35сек.. фантастика. Постгрес 14,15,17.
VladimirFarshatov
Что-то подобное подозревалось после опробывания, замеров не делал, на вскидку не заметил. )