Разработчикам
Производительность и выбор БД
Руководство разработчика PageCharm
Итоги локальных тестов на 100 000 товаров, почему PostgreSQL сейчас быстрее, где хорош SQLite и что еще стоит оптимизировать.
Историческая документация исследовательского стенда. Сам стенд и его данные не входят в очищенный репозиторий 1.0.0; приведённые замеры не являются результатом испытания выпуска 1.0.0.
Короткий вывод
На текущем наборе оптимизаций лучший результат показывает PostgreSQL, затем SQLite, затем MySQL. Это не значит, что MySQL сам по себе медленный: текущая модель запросов PageCharm хранит поля как EAV-атрибуты, а такие запросы сильнее зависят от оптимизатора, индексов и формы SQL. PostgreSQL лучше всего обработал комбинацию фильтров, сортировок и полнотекстового поиска; SQLite выиграл простотой и отсутствием сетевого слоя; MySQL пока страдает на сложных выборках и full-text подзапросах.
Методика
Тестовый проект samples/PageCharm.PerformanceWeb генерирует 100 000 товаров в CMS-блоке perf-catalog. Каждый товар содержит текстовые, числовые, bool, datetime-поля и поле Tags. Замеры идут через HTTP к /api/catalog, то есть через публичный IPageCharmContentQuery, с прогревом и серией повторов. Скрипт сохраняет среднее, p50, p95, минимум и максимум.
Для словарного Select скорость проверяется exact-фильтрами по сохраненному alias, например brand=brand-010. Для настоящих тегов добавлены сценарии tag=promo и tag=promo&brand=brand-010, которые идут через WhereTagsContain и таблицу pagecharm_content_tags.
Цифры ниже — локальные средние значения в миллисекундах. Они нужны не как абсолютный SLA, а как сравнение направления: первый baseline до индексов и текущий результат после typed attribute indexes, точных string/json индексов и full-text read-модели для PostgreSQL/MySQL.
Контрольный прогон 2026-07-06
После добавления словарей, поля Tags и IPageCharmDictionaryQuery был повторен короткий прогон на тех же 100 000 товарах, по 10 повторов на сценарий. Для словарного Select измеряется exact-фильтр по alias (brand), для тегов — WhereTagsContain("tags", "promo"). Во время этих прогонов исправлены два provider-нюанса: SQLite-сортировка DateTimeOffset использует безопасный fallback, а MySQL-фильтр тегов больше не строит проблемный IN @array, вместо этого используется OR-предикат по alias и join по TagId.
| Сценарий IPageCharmContentQuery | SQLite | PostgreSQL | MySQL |
|---|---|---|---|
| Первая страница | 22.80 | 48.78 | 93.02 |
| Глубокая страница | 25.43 | 52.14 | 110.87 |
| Категория + бренд | 37.56 | 24.93 | 825.08 |
| Бренд | 12.29 | 20.56 | 23.69 |
| Тег | 24.16 | 50.03 | 51.08 |
| Тег + бренд | 30.23 | 23.57 | 46.46 |
| Диапазон цены | 250.23 | 84.03 | 170.74 |
| Текстовый поиск | 167.22 | 75.07 | 424.19 |
| Поиск + сортировка | 310.42 | 163.40 | 1044.59 |
| Сортировка по цене | 98.56 | 222.48 | 963.31 |
| Сортировка по datetime | 181.73 | 220.22 | 963.15 |
| Фильтр + сортировка | 256.28 | 130.29 | 1503.84 |
Теговые выборки выглядят здорово на всех трех провайдерах: SQLite около 24-30 мс, PostgreSQL около 24-50 мс, MySQL около 46-51 мс. Это означает, что join-таблица тегов сама по себе не является узким местом. Узкое место MySQL по-прежнему в сложных EAV-комбинациях: категория+бренд, поиск+сортировка и сортировка по полям.
| Raw comparison | SQLite | PostgreSQL | MySQL |
|---|---|---|---|
| raw tag | 12.85 | 41.81 | 846.11 |
| raw tag + brand | 34.11 | 202.41 | 1287.37 |
Raw-проекция не всегда быстрее: на MySQL raw tag-запросы оказались сильно хуже CMS-пути. Поэтому следующий шаг нужно проектировать не как «любой raw быстрее», а как осознанный paged/projection API, который переиспользует отфильтрованный набор id и строит provider-friendly SQL.
Было → стало
| Сценарий | SQLite | PostgreSQL | MySQL |
|---|---|---|---|
| Первая страница | 17.57 → 17.11 | 125.55 → 42.40 | 77.98 → 83.82 |
| Глубокая страница | 38.22 → 22.86 | 156.73 → 46.94 | 142.75 → 110.01 |
| Категория + бренд | 149.88 → 37.09 | 328.94 → 24.80 | 534.82 → 513.48 |
| Диапазон цены | 211.30 → 288.33 | 795.36 → 74.74 | 271.24 → 166.58 |
| Текстовый поиск | 159.47 → 147.07 | 1332.37 → 58.98 | 534.13 → 504.98 |
| Сортировка по цене | 550.25 → 92.59 | 1149.69 → 215.67 | 1002.36 → 878.38 |
| Фильтр + сортировка | 464.18 → 245.90 | 777.65 → 101.63 | 1447.95 → 1050.58 |
Почему MySQL пока хуже
Главная причина — форма запросов, а не «плохой MySQL». Каталог фильтруется по атрибутам, которые лежат в отдельной таблице. Для одного списка получаются несколько EXISTS/JOIN по EAV-атрибутам, сортировка по значению поля и отдельный CountAsync(). MySQL в таких планах чаще уходит в временные таблицы, filesort и повторное чтение подзапросов.
Для текстового поиска ситуация похожая: сейчас страница списка обычно делает два запроса — count и сами товары. Поэтому full-text id-набор может строиться дважды. PostgreSQL с GIN и bitmap scan переносит это лучше, а MySQL FULLTEXT в текущей форме дает улучшение, но не радикальное. Еще один фактор — локальный Docker без тюнинга: размер buffer pool, статистика таблиц и холодность кеша сильнее заметны на MySQL.
Когда выбирать SQLite
SQLite — хороший дефолт для большинства контентных сайтов, документации, корпоративных сайтов, блогов, лендингов и умеренных каталогов до примерно 100 000 страниц или товаров. Он проще всего разворачивается, не требует отдельного DB-сервера, потребляет меньше ресурсов и в read-heavy сценариях показывает очень достойную скорость.
Ограничения SQLite тоже понятные: один активный writer, сложнее горизонтально масштабировать несколько app-инстансов, меньше возможностей для managed backup/HA, а полнотекстовый поиск PageCharm пока использует fallback через LIKE. Если сайт в основном читает данные, а контент редактируется спокойно через админку, SQLite выглядит очень практично.
Когда выбирать PostgreSQL
PostgreSQL сейчас лучший универсальный выбор для production: большие каталоги, активные фильтры и сортировки, поиск, несколько приложений, более строгие требования к надежности и рост выше 100 000 элементов. Он лучше всего проявил себя на текущем benchmark: почти все сложные сценарии ускорились сильнее остальных провайдеров.
Когда выбирать MySQL
MySQL стоит выбирать, если инфраструктура уже построена вокруг него: команда умеет его обслуживать, hosting дает только MySQL, или в компании уже есть мониторинг, backup и регламенты именно под MySQL. Для небольших и средних сайтов он будет работать нормально, но для больших каталогов с активной фильтрацией и сортировкой сейчас требует больше внимания к планам запросов и настройкам.
Что еще оптимизировать
Самый полезный следующий шаг — сделать paged API внутри IPageCharmContentQuery, например ToPageAsync(), чтобы провайдер мог оптимизировать count и выборку страницы вместе или хотя бы переиспользовать найденный id-набор. Это особенно важно для MySQL и full-text поиска.
Второй крупный шаг — projection-режим: выбирать для списка только нужные поля и медиа, например ToListAsync<ProductCard>() или явную проекцию полей. Сейчас публичный query возвращает богатую модель контента, удобную для Razor, но каталогам часто нужны только title, price, cover и URL. Это должно снизить CPU, память и объем данных из БД.
Для самых горячих каталогов можно добавить read-модель под конкретный шаблон: широкую таблицу или materialized view с колонками ContentId, Category, Brand, Price, Rating, SearchText. Это менее универсально, зато дает самый большой запас производительности.
Как сделать тесты объективнее
Для финальных решений стоит прогонять не один короткий benchmark, а серию: 10k, 100k и 500k товаров; warm cache и cold cache; 30–100 повторов; отдельные p95/p99; параллельную нагрузку через k6 или wrk; разные pageSize; сценарии с одной фильтрацией, несколькими фильтрами, поиском, сортировкой и глубоким paging. После миграций на MySQL/PostgreSQL нужно обновлять статистику таблиц.
Пример
# Один и тот же сценарий прогоняется против всех провайдеров.
samples/PageCharm.PerformanceWeb/benchmark-catalog.sh http://127.0.0.1:5105 50
# После больших миграций индексов:
ANALYZE TABLE pagecharm_content_attributes;
ANALYZE TABLE pagecharm_content_search_entries;
ANALYZE TABLE pagecharm_contents;
-- PostgreSQL:
ANALYZE pagecharm_content_attributes;
ANALYZE pagecharm_content_search_entries;
ANALYZE pagecharm_contents;
Практический выбор
Для простого single-instance сайта начните с SQLite. Для большого каталога, поиска и роста берите PostgreSQL. MySQL выбирайте, когда он уже является стандартом инфраструктуры.