Оценка потенциальной производительности информационных систем на задачах OLTP

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

Все мы сегодня наблюдаем неприятное явление деградации эффективности ПО. Эффективность проседает во всём, от пользовательского интерфейса и до того, в чём компьютеры, вроде бы, просто обязаны быть самыми быстрыми, то есть в массовых задачах повседневной обработки информации.

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

Далее показаны потенциальные возможности ПО на самых массовых на сегодня задачах, заключающихся во взаимодействии оператора с информационными системами. Англоязычное название этой группы задач - on-line transaction processing (OLTP). К этому классу, в том числе, относится всё взаимодействие с браузером, которые представляют клиентскую часть системы. За кадром, невидимая для операторов, остаётся почти чистая задача параллельной обработки множества запросов.

Заметим, что для ИТ систем нет разницы, кто с ними взаимодействует - клиент (покупатель) или работник фирмы, поэтому всех пользователей для общности подхода далее будем называть операторами, независимо от вида клиентского ПО, будь то браузер, настольный "толстый" клиент или мобильное приложение.

Но не все операторы равны. Поэтому выделим целевую группу, убрав из неё лишнее. Здесь мы не будем рассматривать аналитико-управленческую часть, а так же игровую или развлекательную. В таких областях взаимодействие с ИТ системами весьма разнообразно и достаточно сложно, но охватить всё и сразу у нас точно не получится.

Определим точнее предмет исследования. Нас интересует оператор, участвующий в коллективном процессе и выполняющий в нём узко специализированные действия. Пример знакомый каждому - интернет-магазин. В магазине покупатель задаёт начальные параметры для дальнейшего процесса обработки товаров или услуг. Далее уже другие участники процесса принимают на себя задачу по достижению общей цели. Кто-то везёт груз, кто-то отвечает на звонки, кто-то следит за синхронностью работы других участников. В целом же ИТ система обеспечивает нужной информацией всех участников в нужное время, а участники дополняют информацию в системе новыми данными о своих действиях.

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

Из сказанного вытекают следующие задачи:

  • ввод и передача информации от оператора

  • хранение информации

  • доступ операторов к информации, определяющей их действия

  • преобразование информации в удобную для операторов форму

Рассмотрим эти задачи с точки зрения производительности ИТ систем

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

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

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

Но соотношение из примера выше - 1 к 1000 - значительно больше аналогичного коэффициента для, например, передачи данных. Если суммарно запрос-ответ занимают 10кб, то на сетевом адаптере производительностью 1 гигабит получаем возможность обработать 10 000 запросов. Но на сервере легко может быть установлено 10 таких адаптеров, что даёт нам уже 100 000 запросов, или соотношение 1 к всего лишь 10. Сравните перегрузку сервера по сетевому обмену с возможной перегрузкой в 1000 раз по обработке запросов. Это сравнение показывает второстепенную роль сетевой нагрузки в задачах OLTP.

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

Моделирование ИТ системы

Для выявления потенциала системы проведём эксперимент на модели. Модель должна максимально близко отражать особенности реальных систем, не смотря на всё их многообразие. Но, с другой стороны, для оценки потенциала нет никакой нужды обращать внимание на разного рода моду, вроде фреймворков, методик и методологий разработки, а так же "передовые" технологии. Сконцентрируемся на главном - записи, хранении и извлечении нужной информации.

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

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

Для учёта разнообразных сущностей, с которыми работают операторы, нам потребуется ещё одна таблица. Для простоты введём аналогию с банком - там есть клиенты и у них есть операции. Таблицу с операциями мы уже создали, осталось создать таблицу с клиентами, и назовём её неожиданно - client. Заметим, что общепринятой практикой именования таблиц является название в единственном числе (client, а не clients), потому что таблица предназначена для учёта сущности "клиент" (обратите внимание - единственно число в названии сущности).

Справочную информацию смоделируем иерархией адресов, начиная от страны и, вниз, до номера дома. Всего получится следующий набор таблиц: country, city, street, address. Такая иерархия хороша количеством таблиц, количеством записей в них, а так же наличием связей между всеми таблицами - опосредовано все таблицы связаны друг с другом.

Логические связи (отношения) между таблицами такие:

Операция относится строго к одному клиенту. У каждого клиента есть адрес, у адреса есть улица, у улицы есть город, у города есть страна, ну а страна у нас одна (одинокая, наверно её никто не любит). Все наши данные можно представить одной цепочкой, предыдущие звенья которой ссылаются на последующие: operation -> client -> address -> street -> city -> country.

Моделирование операций

Теперь определимся с моделью обработки данных. Сначала мы заполняем все справочные таблицы, от клиента до страны. Это просто вставка в таблицы. Она не характерна для задач OLTP, но раз мы её в любом случае вынуждены выполнять, зафиксируем получившийся результат. Затем начинается работа, когда операторы вводят данные операции, выполняемой клиентом. Либо, в случае интернет-магазина, клиент сам является оператором и сам вводит нужную информацию. При этом оператору нужна справочная информация. Во первых, будем предлагать ему данные о клиенте. Во вторых - данные о его операциях. И, дабы более точно имитировать нагрузку, вместе с данными о клиенте и операциях дадим оператору информацию по всей адресной цепочке, до страны, включительно.

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

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

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

Модельная система

Стандартной практикой в проектировании ИТ систем является разделение на работающие на разных серверах компоненты. В нашем случае главным модельным компонентом будет база данных, или точнее - система управления базами данных (СУБД). Второй компонент - условный сервер приложений, или микросервис, если использовать более модную терминологию. Его задача пред- и пост-обработка данных из СУБД. Поскольку этот компонент не хранит информации на медленных устройствах (например на диске), он ограничен только памятью и производительностью процессора. Все медленные операции выполняет СУБД, которая, будучи заточенной на работу с медленными устройствами, в нашей модели может моделировать и операции чтения-записи в файлы, в которых сервера могут хранить какие-то настройки. Остальные задачи - быстрые, то есть быстро выполняемые процессором. Для задач OLTP обычно производительности процессоров хватает с большим запасом. В дальнейшем тестировании так же видно, что по загрузке процессора у нас будет очень большой запас - процессор клиентской части в тестах задействуется максимум на пару процентов. Значит дополнительные расчёты на сервере приложений не смогут существенно исказить результаты моделирования. Поэтому сервер приложений считаем вполне справляющимся с нагрузкой и более не концентрируем на нём своё внимание.

СУБД в нашей модели несёт на себе главную нагрузку, поэтому важно абстрагироваться от её реализации, чего мы достигнем проведением моделирования на двух вариантах СУБД. Первый - весьма распространённая (модная) у нас бесплатная СУБД PostgreSQL. Второй - весьма распространённая в других странах бесплатная СУБД MariaDB (вариант давно известной MySql, более близкий к PostgreSQL по открытости). Первая выбрана из-за распространённости и массового перехода на неё в связи с импортозамещением. Вторая - для сравнения с модной тенденцией, ведь моделей на подиуме должно быть больше одной.

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

Методика оценки производительности

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

Заполнение справочников даёт нам неплохую возможность смоделировать работу системы на массовых (пакетных) операциях. Хоть это и не OLTP, но результат интересен. Время вставки множества записей в одной транзакции (обычная, ускоряющая вставку практика) позволяет относительно точно оценить скорость массовой обработки данных. Операция вставки в таблицу может выполняться по разному в зависимости от дополнительных условий, поэтому часть данных мы вставим при одних условиях, а другую, соответственно, при других, что бы пощупать больше возможных вариантов. Все вставки в справочники будут выполняться в одной транзакции и в одном потоке (без параллельности).

Первый вариант вставки - с одним единственным индексом в виде первичного ключа. Это минимальный набор ограничений. Второй вариант - с ограничением целостности данных по внешнему ключу. Третий вариант - с добавкой дополнительного индекса.

Адресные таблицы заполняются за один подход с уже заданным ограничением по внешнему ключу, чем проверяется скорость варианта номер два. Таблица с клиентами заполняется в трёх подходах: только первичный ключ, добавлен индекс на поле "Имя", добавлено ограничение целостности связи с адресами. Стоит добавить, что ссылки на внешние ключи заполняются в среднем равномерно по всему массиву доступных значений, то есть при чтении данных не возникнет такой ситуации, когда на один адрес ссылаются 100 клиентов, а на другой никто не ссылается.

После заполнения справочников начинается тестирование скорости выполнения рабочих операций. Каждая такая операция включает набор операций помельче, но вся группа выполняется как одно целое, то есть в одной транзакции на групповую операцию. Сначала моделируется распределённая по клиентам нагрузка, то есть на одного клиента приходится мало операций. Это создаёт один вариант нагрузки на СУБД. Затем моделируется локализованная нагрузка, когда на относительно небольшое количество клиентов приходятся все операции. Это второй вариант.

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

Количество записей: В адресах всё начинается со 100 стран и далее на каждом шаге количество записей умножается на 50. То есть на последнем шаге (в таблице адресов) будет 12.5 миллионов записей. Клиенты заполняются в количестве 10 миллионов, партиями по одной трети (3.3 млн) за подход, при этом дополняя СУБД новыми ограничениями на каждом шаге. Распределённая вставка операций выполняется 10 миллионов раз. Локализованная вставка выполняется несколько десятков тысяч раз, в зависимости от количества параллельных потоков, которые может обслужить СУБД при настройках по умолчанию.

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

Моделирование полностью повторялось для каждой СУБД на двух видах дисков - обычном, HDD, и "быстром", то есть на SSD.

Используемое железо

Процессор: i7-9700F 3.00GHz
Память: 32,0 GB
Диск SSD: Samsung SSD EVO 860
Диск HDD: Seagate ST3000DM007

Используемый софт

Windows 11
PostgreSQL 16.1
MariaDB 11.2.2
Java 17

Результат моделирования

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

Ниже представлены результаты измерения скорости операций:

Сводная таблица с данными по однопоточной вставке в справочники и многопоточной вставке в таблицу операций
Сводная таблица с данными по однопоточной вставке в справочники и многопоточной вставке в таблицу операций
Статистика по времени и количеству операций при вставке в справочники (верхняя часть) и таблицу операций (нижняя часть)
Статистика по времени и количеству операций при вставке в справочники (верхняя часть) и таблицу операций (нижняя часть)
Локализованная вставка с разным числом потоков в таблицу операций, расположенную на HDD диске
Локализованная вставка с разным числом потоков в таблицу операций, расположенную на HDD диске
Локализованная вставка с разным числом потоков в таблицу операций, расположенную на SSD диске
Локализованная вставка с разным числом потоков в таблицу операций, расположенную на SSD диске
Время и количество вставок с разным числом потоков на HDD и SSD для MariaDB (верхняя часть) и PostgreSQL (нижняя часть)
Время и количество вставок с разным числом потоков на HDD и SSD для MariaDB (верхняя часть) и PostgreSQL (нижняя часть)
Время операции в зависимости от числа потоков для MariaDB на HDD (количество потоков = число по оси Х умноженное на 10)
Время операции в зависимости от числа потоков для MariaDB на HDD (количество потоков = число по оси Х умноженное на 10)
Время операции в зависимости от числа потоков для MariaDB на SSD (количество потоков = число по оси Х плюс 1, умноженное на 10)
Время операции в зависимости от числа потоков для MariaDB на SSD (количество потоков = число по оси Х плюс 1, умноженное на 10)

Результаты не во всём идеальные, но вроде понятные. Для MariaDB пропущен шаг с 10-ю потоками при работе с SSD. Для PostgreSQL графики строить нет смысла из-за наличия данных лишь по четырём вариантам для количества потоков.

Но пройдёмся по всему по порядку. Вставка в справочники на HDD быстрее выполнена PostgreSQL, но на SSD - MariaDB ведёт с небольшим отрывом. Вставка клиентов на HDD сначала опять выводит вперёд PostgreSQL, и даже с очень большим отрывом при выполнении операции создания ограничения, но вот когда ограничение создано, тут PostgreSQL начинает проигрывать во много раз. На SSD картина меняется - MariaDB обгоняет по большинству операций, а по последней вставке разрыв становится просто неприличным. Хотя время создания ограничения по прежнему сильно больше у PostgreSQL. Но нам ведь интересна работа, а не подготовка к ней.

Запускаем 150 потоков, и MariaDB уверенно рвёт конкурента по времени раз в 5-7, и на SSD, и на HDD. Правда у MariaDB наблюдается непонятная максимальная задержка при вставке в базу с количеством записей до 3.3 млн - почти 10 секунд, такую задержку нельзя списать на случайное использование компьютера для целей, не относящихся к моделированию. Но PostgreSQL при вставке в таблицу с не менее чем 6.6 млн записей показывает гораздо худший результат - максимальное время ожидания аж полторы минуты. Такая пауза заставит большинство операторов начать панические действия по перезагрузке компьютера или что-то подобное. Ну а если такое случится при вызове от сторонней системы, она просто отвалится по таймауту.

В данном тесте выявилось вполне ожидаемое замедление при наполнении базы. Но что интересно, при работе с HDD (в отличии от SSD) замедление оказывается пропорционально корню из отношения количества записей до и после всех вставок. Занимательная загадка для любителей считать О большое в различных алгоритмах.

Тест с локализованными данными проводился с нарастающим числом потоков, пока СУБД не отказывалась принимать соединения. MariaDB приняла все указанные в настройках по умолчанию 150 потоков. И с ростом количества потоков наблюдался, хоть и незначительный при больших значениях числа потоков, но всё же рост производительности. Занимательной особенностью производительности MariaDB является зависимость от каких-то внутренних ресурсов - если число потоков кратно этим ресурсам, наблюдается ускорение, если не кратно - замедление. На графике это выглядит как ломаная линия, похожая на затухающую синусоиду. Опять загадка для умеющих считать О большое.

PostgreSQL не смогла осилить 50 потоков. На части из них драйвер выдал "The connection attempt failed" или "Read timed out". Поэтому более 40 потоков чисто протестировать не удалось. Хотя не исключено, что поковырявшись в настройках эту ситуацию можно исправить. Но MariaDB такого ковыряния не потребовала. У PostgreSQL так же наблюдается рост производительности с ростом числа потоков в варианте с HDD, но вот с SSD имеем спад производительности. Кроме того, в варианте с HDD первая серия вставок с 10 потоками выполнялась намного медленнее остальных серий. Возможно прогревались внутренние кэши. У MariaDB такого единичного скачка не наблюдалось.

Общий итог

Безусловный лидер по рабочим операциям - MariaDB, и на дисках типа SSD, и на HDD. Она же показала наиболее предсказуемые результаты при работе с различным количеством потоков, а так же не удивила нас неожиданными паузами длительностью более 10 секунд. Ещё один хороший сигнал - хоть и затухающий, но рост производительности с увеличением числа потоков. Хотя по отдельным операциям в одном потоке и в одной транзакции PostgreSQL всё же даёт нам какие-то преимущества.

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

Использование модели для оценки ИТ системы банка

Предположим у нас есть банк и мы его автоматизируем. В нашей модели у банка есть 10 миллионов клиентов. Можно достаточно уверенно предположить, что в среднем в один день средний клиент выполнит около двух операций (сам в интернете или опосредовано, через оператора банка). Итого получаем 20 миллионов операций за 86 400 секунд или 232 операции в секунду. Как мы видим из результатов моделирования - даже у аутсайдера на HDD дисках есть запас в 80 операций для покрытия неравномерности нагрузки. Хотя, скорее всего, такого запаса не хватит, но у нас ведь есть вариант с использованием SSD диска. То есть нам хватит одного единственного компьютера с SSD диском для удовлетворения потребностей банка с 10 миллионами клиентов даже на PostgreSQL, если кому-то не нравится победитель - MariaDB.

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

Второй момент - операции бывают разные. Да, бывают, но природа у них у всех одна, её мы обсуждали выше - поиск, группировка, вставка, изменение. Именно такой набор действий и выполняет каждая наша моделируемая операция. Но если в ходе одной банковской операции почему-то нужно не только зафиксировать факт операции и изменить количество денег на счету, но дополнительно требуются ещё обязательные операции, то мы ведь имеем неплохой запас в почти 1800 не занятых операций на PostgreSQL. А на MariaDB запас аж 9300 операций. То есть что бы перегрузить нашу СУБД требуется примерно в 8 с половиной раз больше действий на каждую банковскую операцию, нежели в нашей модели. Ну а MariaDB даёт нам запас аж в 42 раза. Интересно было бы узнать о банке, который требует хотя бы в 8 раз больше действий на каждую операцию, нежели в нашей модели. Хотя если в банке считают все эти дополнительные действия разумными, ну что-ж, у нас всё ещё остаётся MariaDB с её запасом в 41 дополнительную операцию.

Поясним ещё и третий момент. Операции по сети могут быть не быстрыми в сравнении со средним временем выполнения одной операции СУБД (около половины миллисекунды для PostgreSQL и сто микросекунд для MariaDB). В результате в наши операции может быть внесена задержка, длительностью больше, чем длительность операции в СУБД. Но здесь можно смело утверждать - для современных компьютеров это не проблема. Дело в том, что ИТ системы умеют работать с множеством потоков. Это означает, что пока один поток ждёт окончания сетевых операций, другой вполне может начать обработку следующего запроса. Значит, просто увеличивая количество потоков, получаем всё ту же производительность, не смотря на увеличение времени каждой отдельно взятой операции. И да, в ограничения по количеству потоков мы не упрёмся - смотрите количества потоков в тестах, где из доступных на большинстве ОС тысячи или более потоков задействуется максимум 150.

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

И ещё одно сравнение

Для дополнительного подтверждения универсальности предложенной модели попробуем смоделировать интернет-магазин. Возьмём для пример Ozon. Они относительно недавно размещали на Хабре информацию по своей подсистеме учёта доступности товаров и привели ряд интересных цифр. Полный набор операций ими озвучен не был, но мы ведь можем попробовать предположить.

Но сначала о товарах. Поскольку Озон занимается продажей товаров, нам необходимо убедиться, что наша модель адекватно отражает не только операции по продаже, но и поиск по огромным залежам находящегося в продаже ширпотреба. Сколько уникальных товаров продаётся на Озоне? Именно уникальных - немного, несколько десятков тысяч наименований. Но вездесущий маркетинг не даёт покоя продавцам и они изобретают бесконечное количество комбинаций, когда какое-нибудь мыло с полностью идентичной основой продаётся в ассортименте на сотню и более наименований. Всего лишь меняем цвет - и вот уже с десяток новых "мыл". Но можно же менять и запах! Умножаем десяток цветов на 4-5 запахов, и вот вам 40-50 новых наименований. В общем, идею вы поняли - ассортимент на озоне очень большой именно за счёт такого любимого нами маркетинга. Но, с другой стороны, весь этот ассортимент нужно уметь находить и показывать по ограниченному количеству поисковых фраз. Поэтому продавцам приходится как-то сдерживать порыв маркетологов и группировать товары, например, в группу "мыло", увеличивая таким образом вероятность нахождения товара именно теми покупателями, которым нужно мыло, а не конкретно "ландышевое зелёное бактерицидное жидкое в мягкой упаковке со скидкой при покупке двух пакетов весом в 100 грамм". Это означает, что для моделирования всего ассортимента товаров озона нам должно хватить 10 миллионов наименований, которые уже имеются в таблице клиентов. Осталось только представить себе, что вместо клиентов в таблице у нас товары.

Теперь продажи. По открытым данным Ozon делает в среднем около 27 продаж в секунду (209 миллионов заказов за 2-й квартал 23 года). Что включает одна продажа? Очевидно, это поиск клиента на сайте, кликанье на товары, потом переход в корзину и ряд шагов по выбору условий доставки и оплате. Скорее всего каждое добавление в корзину должно быть зафиксировано в СУБД. Если в среднем клиент покупает, допустим, 3 товара за один раз, то мы имеем три вставки в таблицу с операциями с корзиной, 3 изменения статуса товара для резервирования под данного покупателя, ну и три запроса на какие-то справочные данные по товарам. Вместе с покупкой будет 4 операции. К этому минимуму следует добавить несколько десятков запросов для отображения списка товаров, которые обычно сложно найти поиском и приходится долго листать страницы сайта, осуществляя эти самые запросы. В одном запросе обычно (сужу по своему взаимодействию с сайтом) возвращается 36 товаров (4 в строке на 9 строк).

Сравним предположительный объём действий на сайте Ozon с нашей моделью. 4 вставки с 4-мя изменениями аналогичны четырём модельным операциям без дополнительных запросов. Но вполне вероятно, что дополнительные запросы есть, хотя мы и не знаем о их предназначении. Таким образом, 4*27=108 есть наш эквивалент в модельных операциях в секунду. Теперь учтём запросы для поиска. В каждой модельной операции присутствует запрос на сто последних операций со всеми справочниками. Один такой запрос, весьма вероятно, соответствует одному переходу на следующую страницу поиска. Пусть в среднем клиент листает 20 страниц, тогда на одну покупку нам нужно 20*27=540 модельных операций. Присутствующие в модели вставки в данном случае могут моделировать сбор маркетинговой информации о действиях клиента на сайте. Итого имеем 648 модельных операций в секунду.

Смотрим в наши таблицы с результатами моделирования. Опять видим, что PostgreSQL имеет запас в 3.1 раза при работе с SSD, а MariaDB - 14.75 раз.

Но далеко не все клиенты обязательно делают покупки, зайдя на Озон. Сколько есть тех, кто не покупает? Если они не покупают никогда, то зачем ходить на Озон? Если они, например, просто смотрят цены и видят, что в другом месте цены ниже, то может логично смотреть цены именно в этом другом месте? Значит логично предположить, что не покупают, но смотрят товары на Озоне, именно бывшие или будущие покупатели. Сколько раз вы прицениваетесь на Озоне перед покупкой? Ну пусть раз 10. Тогда суммарное количество операций у нас увеличится до примерно 6000 (540*10+648) в секунду. Это число уже не укладывается в запас для PostgreSQL. А MariaDB всё ещё даёт нам возможность в полтора с лишним раза масштабировать нагрузку. На одном компьютере.

Теперь напомню, что по данным из статьи Ozon-а на Хабре, для работы подсистемы учёта наличия товаров задействуются около 700 серверов, плюс ещё пара сотен под разнообразные кэши.

Соглашусь с тем, что озону нужно резервирование, а потому логично выделить под эти нужды сколько-то компьютеров. Затем вспомним о делении на СУБД и сервер приложений, значит нам нужно ещё умножить количество компьютеров на два. Но как Озон получил цифру 900 при весьма вероятно достаточном количестве в, например, 9 компьютеров? В сто раз больше. Интересный вопрос о производительности информационных систем.

Заключение

Производительность современных информационных систем может быть существенно больше в сравнении с распространённой практикой. Но для этого нужен некий финансовый стимул. Никакие разговоры про чьи-то рекорды не убедят менеджеров, пока прибыль не просядет. Но что странно, интернет-магазин Озон убыточен (согласно его финансовой отчётности), а внимания производительности по прежнему уделяет недостаточно (на мой субъективный взгляд, разумеется). В структуре затрат расходы на ИТ-инфраструктуру (расходы на технологии и
контент) представляют из себя 6.6 миллиарда за квартал при убытках в 13.4 миллиарда за тот же квартал. Можно ли сократить убытки в два раза? Если ориентироваться на расходы на технологии и контент, то снизив их до нуля, получим как раз половину от суммы убытков. Разумеется, полностью исключить такие расходы нельзя, но вспоминая о потенциально возможном сокращение количества компьютеров в 100 раз (пусть даже с существенной ошибкой), легко верится, что затраты на ИТ можно сократить эдак раз в 10, и такое сокращение почти равно половине суммы убытков. Конечно же, это очень оптимистичные расчёты, но тем не менее, хочется надеяться, что таким расчётом масштаб проблемы показан достаточно выпукло для привлечения внимания санитаров.

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

Приложение

Все тестовые действия можно выполнить одним запуском прилагаемого Java-класса. Список действий вы найдёте в стандартном для запуска Java-программ методе public static void main(String args[]), при желании можно закомментировать какую-то часть и выполнять операции поотдельности. Для запуска тестов необходимо установить необходимые СУБД, создать в них тестовые схемы, а так же пользователей, которым даны права на изменение DML и DDL операции, после чего задать строки подключения к СУБД в коде (метод getConnectionString) и обеспечить присутствие соответствующих JDBC-драйверов в пути к классам.

Код для моделирования
package test;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.text.DecimalFormat;
import java.util.concurrent.atomic.AtomicInteger;

public class DbTest
{
  public enum Databases { MariaDB, PostgreSQL }

  private static class RRef
  {
    private final static long prime=982_451_653, base=11, startR;
    long r=nextR(startR);
    
    static
    {
      long n=base;
      while (n<prime)
        n=n*base;
      startR=n%prime;
    }
    private static long nextR(long r)
    { return r*base%prime; }
    
    RRef()
    {}
    RRef(long start)
    { r=start; }
    int nextRef(int parentCount)
    {
      while (true)
      {
        long ref=r%parentCount;
        r=nextR(r);
        if (ref>0) return (int)ref;
      }
    }
  }

  public static void main(String args[])
  {
    Databases type=Databases.PostgreSQL;
    int clientInsertCount=3_333_333; // * 3 = 10M
    try
    {
      db(type, DbTest::dropAll);
      db(type, DbTest::dictsCreate);
      db(type, DbTest::dictsInsert);
      db(type, (t,s,c)->clientInsert(clientInsertCount, 12_500_000, t, s, c));
      recreateOperation(type);
      int threadNumber = switch (type)
      {
        case MariaDB -> 150;
        case PostgreSQL -> 33;
      };
      operationsInsert(clientInsertCount/threadNumber,threadNumber,RRef.startR,type,clientInsertCount);
      operationsInsert(clientInsertCount/threadNumber,threadNumber,findShift(type),type,clientInsertCount);
      operationsInsert(clientInsertCount/threadNumber,threadNumber,findShift(type),type,clientInsertCount);
      int maxThreadNumber = switch (type)
      {
        case MariaDB -> 150; // if more then "Too many connections" exception is thrown
        case PostgreSQL -> 60; // if more then "Read timed out" exceptions are thrown
      };
      for (int tn=10;tn<=maxThreadNumber;tn+=10)
        operationsInsert(clientInsertCount/33/tn,tn,0,type,clientInsertCount);
    }
    catch (Exception e) { e.printStackTrace(); }
  }
  
  protected static Void operationsInsert(int insertCount, int threadCount, long start0, Databases dbType, int clientCount) throws SQLException, InterruptedException, ClassNotFoundException
  {
    System.out.println(dbType+" transactions (trans*threads):");
    System.out.println(insertCount+"*"+threadCount);
    long step = shiftRr(insertCount);
    AtomicInteger threadCounter=new AtomicInteger();
    Thread ts[]=new Thread[threadCount];
    for (int i=0;i<ts.length;i++)
    {
      long start = start0==0?RRef.startR:start0;
      start0=start0*step%RRef.prime;
      ts[i]=new Thread(()->
      {
        threadCounter.addAndGet(1);
        try { db(dbType, (t,s,c)->operationsUnit(start,insertCount,clientCount,t,s,c)); }
        catch (ClassNotFoundException | SQLException e) { throw new RuntimeException(e); }
        finally { if (threadCounter.addAndGet(-1)==0) synchronized (ts) { ts.notifyAll(); } }
      },"t"+i);
    }
    long t=System.currentTimeMillis();
    for (int i=0;i<ts.length;i++)
    {
      ts[i].start();
      Thread.sleep(10); // to prevent "socket failed to connect..."
    }
    synchronized (ts) { ts.wait(); }
    t=System.currentTimeMillis()-t;
    DecimalFormat df1=new DecimalFormat("### ### ###.000");
    DecimalFormat df2=new DecimalFormat("### ### ###");
    System.out.println("Total time: "+t+" = "+df1.format(1.0*t/insertCount/threadCount)+"ms/tr = "+df2.format(insertCount*threadCount*1000l/t)+"tr/sec");
    System.out.println();
    return null;
  }

  private static Void operationsUnit(long rrStart, int insertCount, int clientCount, Databases dbType, Statement st, Connection conn) throws SQLException
  {
    try (PreparedStatement pst=conn.prepareStatement("insert into operation (summa, client_id, op_time) values (?,?,?)");
         PreparedStatement pst2=conn.prepareStatement("update operation set summa=? where id=?"))
    {
      RRef rr=new RRef(rrStart);
      long t=System.currentTimeMillis(), maxLocalTime=0, sumLocalTime=0;
      for (int i=0;i<insertCount;i++)
      {
        long localTime=System.currentTimeMillis();
        int clientId=rr.nextRef(clientCount);
        ResultSet rs=st.executeQuery("select * from client c "
            + "left join address a on c.address_id=a.id "
            + "left join street s on s.id=a.street_id "
            + "left join city ci on ci.id=s.city_id "
            + "left join country co on co.id=ci.country_id "
            + "where c.id="+clientId);
        rs.next();
        rs.close();
        int limit=100;
        String limitRows = switch (dbType)
        {
          case MariaDB -> limitRows="limit "+limit;
          case PostgreSQL -> limitRows="limit "+limit;
        };
        rs=st.executeQuery("select o.id, o.*, c.*, a.*, s.*, ci.*, co.* from operation o "
            + "left join client c on o.client_id=c.id "
            + "left join address a on c.address_id=a.id "
            + "left join street s on s.id=a.street_id "
            + "left join city ci on ci.id=s.city_id "
            + "left join country co on co.id=ci.country_id "
            + "where c.id="+clientId+" "
            + "order by o.id desc "+limitRows);
        int k=0, pos=(int)(Math.random()*limit), last=-1;
        while (rs.next())
        {
          last=rs.getInt(1);
          if (k==pos) break;
          k++;
        }
        rs.close();
        if (last>0)
        {
          pst2.setInt(1,pos+20);
          pst2.setInt(2,last);
          pst2.execute();
        }
        pst.setInt(1,(int)(Math.random()*1000+1));
        pst.setInt(2,clientId);
        pst.setTimestamp(3,new Timestamp(System.currentTimeMillis()));
        pst.execute();
        conn.commit();
        localTime=System.currentTimeMillis()-localTime;
        if (localTime>maxLocalTime) maxLocalTime=localTime;
        sumLocalTime=sumLocalTime+localTime;
        if (i>0 && i%10000==0) System.out.println(Thread.currentThread().getName()+": "+i);
      }
      String message=Thread.currentThread().getName()+" is finished, time="+(System.currentTimeMillis()-t)+", maxWait="+maxLocalTime+", avgWait="+sumLocalTime/insertCount;
      System.out.println(message);
    }
    return null;
  }

  protected static void recreateOperation(Databases type) throws SQLException, ClassNotFoundException
  {
    db(type, (t,st,c)->
    {
      switch (type)
      {
        case MariaDB:
          st.execute("drop table if exists operation");
          st.execute("create table operation (id integer AUTO_INCREMENT primary key, summa integer, client_id integer references client (id), op_time timestamp)");
          break;
        case PostgreSQL:
          st.execute("drop table if exists operation");
          st.execute("create table operation (id SERIAL primary key, summa integer, client_id integer references client (id), op_time timestamp)");
          st.execute("create index on operation (client_id)");
          break;
      }
      return null;
    });
  }
  
  protected static void dictsTest(Databases type) throws SQLException, ClassNotFoundException
  {
    System.out.println(type);
    db(type, DbTest::dropAll);
    db(type, DbTest::dictsCreate);
    db(type, DbTest::dictsInsert);
  }

  private static void recreateClientAndOperation(Databases dbType, Statement st, Connection conn) throws SQLException
  {
    switch (dbType)
    {
      case MariaDB:
        st.execute("drop table if exists operation");
        st.execute("drop table if exists client");
        st.execute("create table client (id integer AUTO_INCREMENT primary key, address_id integer, name varchar(30), last_name varchar(30))");
        st.execute("create table operation (id integer AUTO_INCREMENT primary key, summa integer, client_id integer references client (id), op_time timestamp)");
        break;
      case PostgreSQL:
        st.execute("drop table if exists operation");
        st.execute("drop table if exists client");
        st.execute("create table client (id SERIAL primary key, address_id integer, name varchar(30), last_name varchar(30))");
        st.execute("create table operation (id SERIAL primary key, summa integer, client_id integer references client (id), op_time timestamp)");
        st.execute("create index on operation (client_id)");
        break;
    }
    conn.commit();
  }
  
  protected static Void dictsCreate(Databases dbType, Statement st, Connection conn) throws ClassNotFoundException, SQLException
  {
    switch (dbType)
    {
      case MariaDB:
        st.execute("drop table if exists address");
        st.execute("drop table if exists street");
        st.execute("drop table if exists city");
        st.execute("drop table if exists country");
        st.execute("create table country (id integer AUTO_INCREMENT primary key, country_code varchar(4), name varchar(150))");
        st.execute("create table city (id integer AUTO_INCREMENT primary key, country_id integer references country (id), name varchar(150))");
        st.execute("create table street (id integer AUTO_INCREMENT primary key, city_id integer references city (id), name varchar(150))");
        st.execute("create table address (id integer AUTO_INCREMENT primary key, street_id integer references street (id), flat integer, building varchar(5), house varchar(5))");
        break;
      case PostgreSQL:
        st.execute("drop table if exists address");
        st.execute("drop table if exists street");
        st.execute("drop table if exists city");
        st.execute("drop table if exists country");
        st.execute("create table country (id SERIAL primary key, country_code varchar(4), name varchar(150))");
        st.execute("create table city (id SERIAL primary key, country_id integer references country (id), name varchar(150))");
        st.execute("create table street (id SERIAL primary key, city_id integer references city (id), name varchar(150))");
        st.execute("create table address (id SERIAL primary key, street_id integer references street (id), flat integer, building varchar(5), house varchar(5))");
        st.execute("create index on city (country_id)"); // Postres doesn't create indexes on fk columns while other DBs do
        st.execute("create index on street (city_id)");
        st.execute("create index on address (street_id)");
        break;
    }
    return null;
  }

  protected static Void dropAll(Databases dbType, Statement st, Connection conn) throws SQLException, ClassNotFoundException
  {
    switch (dbType)
    {
      case MariaDB:
      case PostgreSQL:
        st.execute("drop table if exists operation");
        st.execute("drop table if exists client");
        st.execute("drop table if exists address");
        st.execute("drop table if exists street");
        st.execute("drop table if exists city");
        st.execute("drop table if exists country");
        break;
    }
    return null;
  }
  
  protected static Void clientInsert(int clientCount, int addressCount, Databases dbType, Statement st, Connection conn) throws ClassNotFoundException, SQLException
  {
    System.out.println(dbType);
    long t=System.currentTimeMillis();
    
    recreateClientAndOperation(dbType, st, conn);

    t = time(t,"Preparation");
    
    PreparedStatement pst=conn.prepareStatement("insert into client (name, last_name, address_id) values (?,?,?)");
    insertClients(addressCount, clientCount, conn, pst);
    
    t = time(t,"Insert1");
    
    st.execute("create index name on client (name)");
    conn.commit();
    
    t = time(t,"Indexing");

    insertClients(addressCount, clientCount, conn, pst);
    
    t = time(t,"Insert2");
    
    st.execute("alter table client add constraint contr_address foreign key (address_id) references address (id)");
    if (dbType==Databases.PostgreSQL) st.execute("create index on client (address_id)");
    conn.commit();
    
    t = time(t,"Altering table");

    insertClients(addressCount, clientCount, conn, pst);

    t = time(t,"Insert3");
    return null;
  }
  
  protected static Void dictsInsert(Databases dbType, Statement st, Connection conn) throws ClassNotFoundException, SQLException
  {
    byte bs[]=new byte[150];
    int countryCount=100, factor=50;
    long t=System.currentTimeMillis();
    PreparedStatement pst=conn.prepareStatement("insert into country (name, country_code) values (?,?)");
    insertRecords(100, 1, 150, pst, conn, bs, (i,ps)->ps.setString(2,""+(i%10_000)));
    t = time(t,"Countries");
    conn.commit();
    pst=conn.prepareStatement("insert into city (name, country_id) values (?,?)");
    insertRecords(countryCount, factor, 150, pst, conn, bs);
    t = time(t,"Cities");
    conn.commit();
    pst=conn.prepareStatement("insert into street (name, city_id) values (?,?)");
    insertRecords(countryCount*factor, factor, 150, pst, conn, bs);
    t = time(t,"Streets");
    conn.commit();
    pst=conn.prepareStatement("insert into address (building, street_id, house, flat) values (?,?,?,?)");
    insertRecords(countryCount*factor*factor, factor, 5, pst, conn, bs, (i,ps)->ps.setString(3,""+(i%100_000)), (i,ps)->ps.setInt(4,i));
    t = time(t,"Addresses");
    conn.commit();
    return null;
  }
  
  private interface ConsumerWithException
  { void accept(int i, PreparedStatement pst) throws SQLException; }
  
  private static void insertRecords(int parentCount, int factor, int strLen, PreparedStatement pst, Connection conn, byte[] bs, ConsumerWithException ... fieldValues) throws SQLException
  {
    RRef rr=new RRef();
    for (int i=0;i<parentCount*factor;i++)
    {
      String s=generateRandomString(strLen,bs,0);
      pst.setString(1,s);
      if (factor>1) pst.setInt(2,rr.nextRef(parentCount));
      for (ConsumerWithException r:fieldValues)
        r.accept(i,pst);
      pst.execute();
    }
    conn.commit();
  }

  private static void insertClients(int addressCount, int clientCount, Connection conn, PreparedStatement pst) throws SQLException
  {
    RRef rr=new RRef();
    byte bs[]=new byte[30];
    for (int i=0;i<clientCount;i++)
    {
      pst.setString(1,generateRandomString(30,bs,0));
      pst.setString(2,generateRandomString(30,bs,0));
      pst.setInt(3,rr.nextRef(addressCount));
      pst.execute();
    }
    conn.commit();
  }

  private interface Task<T>
  { T run(Databases dbType, Statement st, Connection conn) throws SQLException, ClassNotFoundException; }
  
  protected static <T> T db(Databases dbType, Task<T> task) throws SQLException, ClassNotFoundException
  {
    String connectionString = getConnectionString(dbType);
    try (Connection conn = DriverManager.getConnection(connectionString))
    {
      Statement st=conn.createStatement();
      conn.setAutoCommit(false);
      T t=task.run(dbType,st,conn);
      conn.commit();
      return t;
    }
  }

  private static String getConnectionString(Databases db) throws ClassNotFoundException
  {
    switch (db)
    {
      case MariaDB: Class.forName("org.mariadb.jdbc.Driver"); break;
      case PostgreSQL: Class.forName("org.postgresql.Driver"); break;
    }
    String connectionString = switch (db)
    {
      case MariaDB -> "jdbc:mariadb://localhost:3306/test_db?user=aaa&password=12";
      case PostgreSQL -> "jdbc:postgresql://localhost:5432/test_db?currentSchema=test_schema&user=aaa&password=12";
    };
    return connectionString;
  }

  private static long time(long t, String mark) {
    t=System.currentTimeMillis()-t;
    System.out.println(mark+": "+t);
    return System.currentTimeMillis();
  }
  
  public static String generateRandomString(int length, byte buffer[], int bufferOffset)
  {
    int n=length/8;
    if (length%8>0) n++;
    if (buffer==null) buffer=new byte[length];
    for (int j=0;j<n;j++)
    {
      long x=(long)(Math.random()*Long.MAX_VALUE);
      for (int k=0;k<8;k++)
      {
        int pos=j*8+k;
        if (pos>=length) break;
        pos=pos+bufferOffset;
        buffer[pos]=(byte)((x|32)&127);
        if (buffer[pos]==127) buffer[pos]=126;
        x=x>>8;
      }
    }
    return new String(buffer,0,length);
  }

  protected static long findShift(Databases type) throws SQLException, ClassNotFoundException
  {
    return db(type, (t,st,c)->
    {
      ResultSet rs=st.executeQuery("select count(*) from operation");
      rs.next();
      int n=rs.getInt(1);
      rs.close();
      return shiftRr(n);
    });
  }

  private static long shiftRr(int insertCount)
  {
    RRef rr=new RRef();
    for (int i=0;i<insertCount;i++)
      rr.nextRef(111);
    return rr.r;
  }
}

Цель выкладывания класса с набором тестов состоит в уменьшении количества споров при интерпретации результатов. Все параметры СУБД, как было сказано ранее, остались равны значениям по умолчанию. Кроме параметров СУБД на результат влияют два фактора - сам набор тестов и тестовая машина. Используя один и тот же набор тестов (в прилагаемом классе), а так же одинаковые версии СУБД, мы должны получить одинаковые результаты на одинаковом железе. Поскольку железо, скорее всего, у всех будет разным, можно предложить следующую методику для получения однозначных результатов:

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

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

Источник: https://habr.com/ru/articles/789074/


Интересные статьи

Интересные статьи

Статья адресована администраторам почтовых систем, задумывающимся о замене существующей или внедрении новой почтовой системы. Почему именно RuPost? Потому, что про CommuniGate Pro я уже писал, а други...
Аналитики строили систему рекомендаций для менеджеров по работе с авиакомпаниями. Рекомендация должна помочь менеджеру вовремя заметить отклонения в показателях авиакомпании, оперативно отреагировать ...
Всем привет, меня зовут Таня, я системный аналитик в МТС Финтех. Работаю в команде Персонализации, которая отвечает за несколько виджетов на главном экране мобильного приложения МТС Банк.В этой статье...
Несмотря на то, что MariaDB является форком базы данных MySQL Oracle, они разошлись настолько, что сейчас сильно отличаются друг от друга. Такая система управления базами данных, как MySQL, является п...
Если вы пользуетесь Linux с ранних дней появления этой ОС (или если, вроде меня, начинали с Unix), то вам не надо очень быстро и в больших количествах изучать то новое, что появляется в с...