MySQL vs MongoDB для звітності та аналітики: чому команди повертаються до SQL

Було корисно?

Якщо ви колись бачили, як CEO оновлює дашборд, поки ваш on-call телефон гріється в руці, ви знаєте цей біль: трафік звітності не падає плавно. Він приходить гуртом, просить «ще один злам» і тоді ваша база починає видавати незнайомі звуки.

Команди люблять MongoDB за швидку доставку фіч продукту. Потім з’являється звітність. Раптом ви налагоджуєте агрегатні конвеєри о 2:00 ночі, і всі починають з ностальгією згадувати SQL-з’єднання, про які раніше жалілися.

Чому команди повертаються до SQL (і чому це не лицемірство)

Звітність та аналітика — це інший вид спорту, ніж OLTP. Ваша продуктова база побудована для невеликих передбачуваних читань і записів: отримати користувача, оновити кошик, вставити замовлення. Звітність побудована для великих сканів, складних групувань, широких фільтрів і «порівняй цей місяць із минулим по шести вимірах». Вона також орієнтована на людей із часовими зонами, думками та Excel.

MongoDB може виконувати аналітику, особливо з aggregation framework, але багато команд виявляють одну незручну правду: документні сховища оптимізовані під доступи форми застосунку, а не під запити аналітика. Аналітики не думають у термінах «документів»; вони думають у термінах відношень і вимірів. Вони поставлять питання, яким ваш початковий дизайн документів активно опирається.

SQL-системи (MySQL, Postgres, SQL Server тощо) мають довгу, сувору історію підлаштування під звітність. Ця історія важлива. Оптимізатори, стратегії з’єднань, стабільна семантика, зрілі інструменти — це накопичена операційна «шрамована» мудрість. MongoDB покращується, але коли ваш VP хоче діаграму когорт до полудня, ви оберете нудні, зрозумілі, добре протоптані шляхи.

Також: звітність — це політика. Люди хочуть однакових чисел у різних командах. Це означає узгоджені визначення. SQL-схеми та view перетворюють визначення на спільні, рев’ювані артефакти. MongoDB часто переводить їх у фрагменти коду застосунку й конвеєри агрегування, розсипані по репозиторіях, як блискітки — назавжди.

Короткий жарт, для настрою: MongoDB безсхемна так само, як кімната підлітка — без меблів. Технічно правда, операційно дорого.

Історичний контекст та цікаві факти (те, що люди забувають)

Ось кілька конкретних фактів і хронологічних штрихів, що пояснюють, чому SQL постійно «повертає» робочі навантаження звітності. Не відчуття — реальна історія.

  1. Стандартизація SQL почалася в 1980-х. Це означає десятиліття інструментів: JDBC/ODBC, BI-конектори, планувальники запитів і спільна ментальна модель у компаніях.
  2. MySQL з’явився в середині 1990-х і став базою по замовчуванню для веб-проєктів покоління. Шаблони звітності були вивчені у продакшені важким шляхом: репліки, summary tables, rollups і партиціонування.
  3. MongoDB виникла близько 2009 року під час хвилі NoSQL-систем, орієнтованих на горизонтальне масштабування і швидкий реліз. Багато з них були спроектовані передусім для робочих навантажень застосунків, а не для довільної аналітики.
  4. Виникнення JSON (і пізніше JSON у SQL) розмило межу. SQL-системи додали JSON-стовпці та функції; MongoDB розширив можливості запитів і агрегацій. Зближення відбулося, але нерівномірно.
  5. «Схема-при-читанні» стала популярною в епосі big data (Hadoop-ерa). Вона працювала для пакетної обробки, але оперативна звітність хоче швидких інтерактивних запитів. Схема-при-читанні рідко буває безкоштовною.
  6. Колонкове зберігання та дата-склади змінили очікування. Команди звикли до дашбордів, що швидко сканують мільйони рядків. Спробувавши це, документне сховище для широких group-by може відчуватися як рух велосипедом вантажівкою.
  7. BI-інструменти еволюціонували навколо SQL. Навіть коли інструменти підтримують MongoDB, «золоті» функції (семантичні шари, кешування, lineage, permissioning) майже завжди SQL-перші.
  8. Патерни реплікації старші за більшість мікросервісів. Реплікація MySQL і патерни масштабування на читання глибоко вивчені операційно, включно з режимами відмов і playbook-ами відновлення.

Все це зводиться до простого операційного правила: успіх у звітності — це не стільки «чи може база виконати запит», скільки «чи може ваша організація експлуатувати базу під непередбачуваним людським навантаженням». SQL-системи довше перебували в цій арені.

Реальність навантаження звітності: що дашборди роблять з базами

Запити звітності грубі. Вони не використовують ваші ретельно продумані первинні ключі. Вони не поважають кеш. Вони люблять часові діапазони. Вони хочуть групувати по стовпцях, які ви не індексували, бо їх не було у v1.

Три речі, які роблять дашборди і що порушують припущення «бази застосунку»

  • Вони викликають фановий розгалуження (fan-out): одне завантаження сторінки запускає багато запитів (фільтри, підсумки, графіки, drilldown). Помножте на понеділковий ранковий наплив.
  • Вони змінюють моделі доступу: замість «отримати документ за id» ви отримуєте «сканувати останні 90 днів і порахувати розподіл».
  • Вони вимагають послідовності визначень: «активний користувач», «дохід», «відтік», «конверсія» означають різні речі, поки ви їх не закріпите. SQL-схеми, view і збережена логіка допомагають це забезпечити.

Аналітика — це не лише швидкість запиту

Швидкість важлива, але також:

  • Передбачуваність: стабільна продуктивність запитів при різних параметрах і зростанні даних.
  • Пояснюваність: потрібно показати, як було пораховано число, а не лише його доставити.
  • Операційна ізоляція: звітність не повинна вражати ваш платіжний потік.
  • Управління схемою: якщо ваші «події» мають 19 форм, хтось точно порахує DAU неправильно.

Є причина, чому багато майбутніх компаній запускають звітність на репліках, у сховищах або окремих дата-мартах. Вони навчилися, що змішувати OLTP і ad-hoc аналітику — це як пускати клієнтів у кухню під час вечірньої шпіки.

Чому MySQL зазвичай виграє для звітності

1) З’єднання — це фіча, а не моральна невдача

Звітність за своєю суттю реляційна: замовлення пов’язані з клієнтами, клієнти — з сегментами, сегменти — з кампаніями, кампанії — з витратами. Навіть якщо застосунок зберігає «знімок клієнта» в документі замовлення, аналітики все одно попросать атрибути на рівні клієнта, оновлені сегментації і корекції. З’єднання — це спосіб пов’язати факти і виміри без переписування історії.

Движок з’єднань, оптимізатор і модель індексів MySQL побудовані навколо цього. Коли ви пишете запит, що з’єднує п’ять таблиць із селективними предикатами, MySQL має шанс робити щось розумне. В MongoDB ви часто або денормалізуєте (і заплатите пізніше), або використовуєте $lookup (і тоді знову відкриваєте для себе з’єднання, але з меншим набором запобіжників і іноді з пам’ятними проблемами).

2) Плани запитів можна інспектувати і діяти операційно

У MySQL EXPLAIN, гістограми та трейси оптимізатора дають структурні підказки. Ви можете спитати «чому обрано цей індекс», «чому відбулося сортування», «чому було сканування» і отримати відповідь, яка відповідає відомим патернам тонкого налаштування.

Це важливо під час інцидентів. Коли дашборди тануть у продакшені, вам не потрібні філософські дебати. Вам потрібно побачити план, виправити індекс, додати покриваючий індекс, переписати запит або перемістити навантаження. Швидко.

3) Екосистема невблаганно SQL-орієнтована

BI-інструменти, інструменти управління, патерни доступу, аудит, логування запитів і навіть «як навчати новачків робити звітність» простіше, коли шар зберігання — SQL. Ваш фінансовий відділ не хоче вивчати aggregation pipelines. Вони хочуть view під назвою revenue_daily.

4) Репліки, партиціонування і summary tables — нудні, але ефективні

Звітність у MySQL зазвичай закінчується комбінацією:

  • читальні репліки для трафіку дашбордів
  • партиціонування фактних таблиць (часто за датою)
  • попередньо агреговані summary tables для гарячих метрик
  • матеріалізовані rollup-и (так, ви будуєте міні-склад)

Це не трендово. Це, однак, те, як ви тримаєте продукт у роботі, поки орган ставить питання.

Де MongoDB дійсно хороша (і де ні)

MongoDB блищить, коли форма документу відповідає формі запиту

Якщо ваша звітність — «показати профіль користувача з вкладеними налаштуваннями та недавньою активністю» і ви побудували документи саме для цього, MongoDB може бути швидкою і приємною. Агрегації по одній колекції з селективними фільтрами і правильними індексами також можуть працювати добре.

MongoDB має проблеми, коли звітність стає крос-суб’єктною і еволюційною

Режим відмови виглядає так:

  • Дані спочатку денормалізували для швидкості.
  • Визначення еволюціонують («активний» змінюється, сегментація змінюється, з’являються корекції).
  • Тепер треба бекфіллити вкладені поля, узгоджувати несумісності і пояснювати, яка версія істини використовується для кожної діаграми.
  • Хтось вводить $lookup pipeline-и для «з’єднання» колекцій.
  • Сплески пам’яті, проміжні результати розростаються, і кластер поводиться так, ніби в нього алергія на вашого CFO.

MongoDB для звітності може бути прийнятною, якщо використовувати її як джерело, а не як рушій звітів

Дуже звична стабільна архітектура виглядає так: MongoDB для OLTP-подібних документних випадків → стрім/ETL у SQL-систему або сховище для звітності. Це не образа на MongoDB. Це спеціалізація. Так само, як ви не запускаєте контрольну площину Kubernetes на тих самих вузлах, що ваші пакетні Spark-завдання, якщо вам не подобається хаос.

З’єднання проти вкладення: податок звітності, який ви все одно заплатите

Вкладення спокусливе. Один чит, все потрібне — жодних з’єднань. Чудово — поки бізнес не захоче історію й консистентність. Як тільки ви вкладете змінні атрибути (рівень клієнта, адреса, сегмент) у документоподібні факти (замовлення, події), ви обираєте модель версіонування. Зазвичай це «те, що було під час запису». Іноді це правильно. Часто — ні.

Змінні виміри — там, де денормалізація NoSQL болить найбільше

Звітність хоче мати можливість запитати:

  • «Який був дохід за поточним сегментом?»
  • «Який був дохід за сегментом на момент покупки?»
  • «Що станеться, якщо ми перекласифікуємо цих клієнтів?»

У SQL це рішення схеми: slowly changing dimensions, snapshot tables або історія типу 2. В MongoDB команди часто хакують це, зберігаючи кілька полів, бекфіллячи або пишучи ad-hoc reconciliation-пайплайни. Це може працювати, але менш явно і простіше зробити помилку.

Другий короткий жарт (і все)

Денормалізація — це як пакуватися в подорож, приміряючи всі сорочки одночасно. Ви заощадите місце в багажі, поки не треба переодягатися.

Плани запитів, індекси і чому «в staging працювало» — це брехня

Staging-дані ввічливі. Продакшн-дані творчі. Запити аналітики підсилюють цю творчість, бо вони зачіпають більше рядків, ширші діапазони і дивні куточки даних.

MySQL: передбачувані режими відмов

У інцидентах звітності MySQL звичайні підозрювані:

  • Відсутність складених індексів для типових патернів фільтр+групування
  • Сортування великих результатів через ORDER BY по неіндексованих стовпцях
  • Помилки порядку з’єднань, коли статистика застаріла або спектр даних викривлений
  • Витік тимчасових таблиць на диск під час агрегацій
  • Затримка реплік, коли дашборди штурмують репліки

Хороша новина: ці проблеми діагностуються стандартними інструментами і зазвичай виправляються без повного редизайну моделі даних.

MongoDB: круті падіння продуктивності через агрегації й пам’ять

MongoDB може поводитися добре, поки раптом ні. Типові обриви:

  • $group з великою кардинальністю ключів створює величезний стан в пам’яті
  • $lookup з’єднання вибухають проміжними результатами
  • Використання індексів добре для першого $match, а далі — у вас pipeline-територія
  • Запити повільнішають, коли документи стають різноманітнішими (широкі документи, масиви)
  • Ключі шардингу, оптимізовані для OLTP, не відповідають фільтрам звітності

Цитата про надійність (парафразована ідея)

Парафразована ідея, приписана Річарду Куку: «Успіх в операціях часто ховає роботу, яка робить так, щоб відмови не траплялися.»

Звітність — це місце, де прихована робота стає видимою. Якщо ви хочете дашборди, які не соромлять вас, потрібно явне проектування: схеми, індекси, ізоляція та рутинне обслуговування.

Три корпоративні міні-історії з практики

Міні-історія 1: інцидент через хибне припущення

Компанія A запускала основний продукт на MongoDB. Замовлення були документами з вкладеним знімком клієнта: ім’я, email, рівень, регіон. Припущення було просте: «Ми завжди хочемо інформацію про клієнта такою, якою вона була під час замовлення». Це було вірно для чеків і підтримки, тому всі погодилися і рухалися далі.

Потім бізнес запустив проєкт перекласифікації з огляду на відповідність. Регіони змінилися. Клієнтів переназначили. Фінанси захотіли дохід за поточним регіоном за останні вісім кварталів відповідно до нових звітів. Команда дашбордів побудувала агрегатний pipeline по колекції orders і згрупувала по вкладеному полю region. Він повернув числа. Вони були неправильними. Потайки неправильними.

Коли розбіжність спливла, інженери намагалися «поправити дані», бекфіллячи вкладені поля region по історичних замовленнях. Це зайняло дні, навантажило кластер і порушило початкове припущення для підтримки: чек тепер показував регіон, в якому клієнт не проживав під час покупки. Дві істины, одне поле.

Кінцеве рішення було не гламурним: нормалізувати виміри у SQL-репортинговому сховищі, зберігати факти замовлень незмінними і змоделювати регіон як повільно змінний вимір з явними «as-of» з’єднаннями. Репортинговий шар отримав два view: «region_at_purchase» і «region_current». Дашборд перестав брехати. Люди перестали кричати. Це виглядає як дорослість у системах даних.

Міні-історія 2: оптимізація, що відрізалася бумерангом

Компанія B використовувала MySQL і зіткнулася з латентністю звітності. Хтось запропонував оптимізацію: «Ми щовечора передкомп’ютуватимемо все в одну широку таблицю, щоб дашборди ніколи не з’єднувалися.» Звучало ефективно. Також звучало як ідея втомленої команди, яка довго отримувала сповіщення.

Широка таблиця працювала близько двох тижнів. Потім лонч продукту приніс нові атрибути. ETL ускладнився умовною логікою. Колонки множилися. Таблиця стала настільки широкою, що більшість запитів читали набагато більше даних, ніж треба. Innodb buffer pool почав інтенсивно трястися. Реплікаційна затримка зросла, бо нічна задача записувала величезний обсяг даних у короткий час.

Гірше: оскільки задача виконувалася щодня вночі, бізнес почав очікувати «сьогоднішні» числа о 10 ранку. Вони отримували вчорашні. Команди почали запускати часткові бекфілли вдень. Це співпало з піковим трафіком і створило прогнозований щоденний інцидент. «Оптимізація» перетворилася на операційний графік болю.

Відновлення було простим: повернутися до нормалізованих fact і dimension таблиць, побудувати невелику кількість цілеспрямованих summary tables для справді гарячих метрик і оновлювати їх інкрементально (погодинно або стрімінгом) з идемпотентною логікою. Дашборди прискорились, і система припинила давати щоденні сюрпризи.

Міні-історія 3: нудна, але правильна практика, що врятувала день

Компанія C мала і MongoDB (продукт), і MySQL (звітність). Практика, що їх врятувала, була не якоюсь вишуканою архітектурою. Це був щотижневий ритуал: перегляд slow query log, перегляд графіків затримки реплік і запуск невеликого набору «топ-запитів дашбордів» проти даних, схожих на продакшн, з захопленими EXPLAIN/explain().

Це було нудно. Але воно створило спільну мову між аналітиками даних і SRE. Аналітики навчилися формулювати питання так, щоб не доводилося сканувати всесвіт. SRE дізналися, які метрики критично важливі для бізнесу і заслуговують summary tables або виділених реплік.

Коли компанія отримала сплеск трафіку від неочікуваного партнерства, навантаження на звітність різко зросло. On-call інженер уже знав два найгірші запити, індекси, які вони потребують, і безпечні обмежувачі. Він увімкнув read-only reporting replica для дашбордів і обмежив швидкість endpoint-у «download CSV», який мав звичку робити повні сканування таблиць.

Без героїзму. Жодних all-hands. Просто передбачувані операції, побудовані на нудній звичці. Лонч партнерства став історією про дохід, а не про падіння сервісу.

Практичні завдання (команди, виводи та рішення)

Це реальні завдання, які ви можете виконати сьогодні. Кожне включає: команду, що означає вивід, і рішення, яке з цього випливає. Використовуйте їх як робочий чекліст, а не як факультатив.

Завдання 1: MySQL — ідентифікувати найвитратніші шаблони запитів

cr0x@server:~$ sudo pt-query-digest /var/log/mysql/mysql-slow.log | head -n 40
# 120s user time, 2.1s system time, 28.50M rss, 120.00M vsz
# Query_time distribution
#   1us
#  10ms
# 100ms  ####
#    1s  ##########
#   10s  #####
# Profile
# Rank Query ID           Response time Calls R/Call  V/M   Item
# ==== ================== ============= ===== ======= ===== ============
#    1 0xA1B2C3D4E5F6A7B8  420.1234  1200 0.3501  0.02  SELECT orders...
#    2 0x1122334455667788  180.0000   200 0.9000  0.01  SELECT users...

Значення: Ви отримуєте, які форми запитів коштують найбільше загального часу, а не лише одиничний найповільніший. Більшість болю звітності — це «помірно повільний запит, який викликають часто».

Рішення: Виправте rank #1–#3 спочатку: додайте індекси, перепишіть або перемістіть їх на репліку/summary table. Ігноруйте рідкісний «єдиноріг», поки не припиниться масова проблема.

Завдання 2: MySQL — перевірити затримку реплік перед тим, як звинувачувати запит

cr0x@server:~$ mysql -e "SHOW SLAVE STATUS\G" | egrep "Seconds_Behind_Master|Slave_IO_Running|Slave_SQL_Running"
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Seconds_Behind_Master: 187

Значення: Ваша репліка для звітів відстає приблизно на 3 хвилини. Дашборди можуть показувати «неправильні» числа і аналітики будуть скаржитися на багу в даних.

Рішення: Якщо лаг неприйнятний, зменшіть тиск записів (батчеві ETL), збільшіть потужність репліки або направте деякі дашборди до кешованих результатів. Не оптимізуйте запит на репліці, яка тоне в apply lag.

Завдання 3: MySQL — перевірити, що оптимізатор насправді робить

cr0x@server:~$ mysql -e "EXPLAIN FORMAT=tree SELECT c.segment, DATE(o.created_at) d, SUM(o.total) s FROM orders o JOIN customers c ON c.id=o.customer_id WHERE o.created_at >= '2025-01-01' AND o.created_at < '2025-02-01' GROUP BY c.segment, d;"
EXPLAIN: -> Aggregate using temporary table  (cost=...)
    -> Nested loop inner join  (cost=...)
        -> Index range scan on orders using idx_orders_created_at  (cost=...)
        -> Single-row index lookup on customers using PRIMARY (id=o.customer_id)

Значення: Запит використовує range scan по orders.created_at, потім PK lookup-и до customers, а потім тимчасову таблицю для групування.

Рішення: Якщо тимчасова таблиця велика, розгляньте складений індекс для підтримки group-by патернів (або summary table). Якщо range scan надто широкий — партиціонувати по даті або звузити предикати.

Завдання 4: MySQL — дізнатися, чи тимчасові таблиці витікають на диск

cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Created_tmp%';"
+-------------------------+----------+
| Variable_name           | Value    |
+-------------------------+----------+
| Created_tmp_disk_tables | 48219    |
| Created_tmp_tables      | 51200    |
+-------------------------+----------+

Значення: Велика частина тимчасових таблиць приходить на диск, часто через великі сортування/групування.

Рішення: Оптимізуйте запити/індекси, щоб зменшити кількість тимчасових таблиць, або обережно підвищуйте ліміти пам’яті. Для звітності краще віддавати перевагу попередній агрегації, а не «збільшенню тимчасового простору» як стратегії.

Завдання 5: MySQL — перевірити тиск на buffer pool (чи ви щось кешуєте?)

cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests';"
+--------------------------+-----------+
| Variable_name            | Value     |
+--------------------------+-----------+
| Innodb_buffer_pool_reads | 9812234   |
+--------------------------+-----------+
+----------------------------------+-------------+
| Variable_name                    | Value       |
+----------------------------------+-------------+
| Innodb_buffer_pool_read_requests | 2048123456  |
+----------------------------------+-------------+

Значення: Багато логічних зчитувань обслуговуються з пам’яті; деякі фізичні зчитування все ще відбуваються. Якщо Innodb_buffer_pool_reads різко зростає під час використання дашбордів, у вас буде thrashing.

Рішення: Розгляньте відокремлення звітності на репліки з більшим об’ємом пам’яті, додайте покриваючі індекси або віддайте навантаження сховищу. Не дозволяйте дашбордам витісняти гарячі сторінки OLTP.

Завдання 6: MySQL — підтвердити, чи є у вас правильний складений індекс (або ні)

cr0x@server:~$ mysql -e "SHOW INDEX FROM orders;"
+--------+------------+------------------------+--------------+-------------+-----------+-------------+
| Table  | Key_name   | Column_name            | Seq_in_index | Cardinality | Non_unique| Index_type  |
+--------+------------+------------------------+--------------+-------------+-----------+-------------+
| orders | PRIMARY    | id                     | 1            | 50000000    | 0         | BTREE       |
| orders | idx_orders_created_at | created_at   | 1            | 3650        | 1         | BTREE       |
| orders | idx_orders_customer_id| customer_id  | 1            | 2000000     | 1         | BTREE       |
+--------+------------+------------------------+--------------+-------------+-----------+-------------+

Значення: У вас є індекси по одному стовпцю, але немає складеного, наприклад (created_at, customer_id) або (created_at, status), який відповідав би фільтрам звітності.

Рішення: Додайте складені індекси для топових предикатів звітності. Робіть це на репліці спочатку, вимірюйте ефект, а потім впроваджуйте. Індекси не безкоштовні — оптимізуйте під фактичні запити.

Завдання 7: MongoDB — знайти важкі агрегації й чи вони витікають на диск

cr0x@server:~$ mongosh --quiet --eval 'db.orders.explain("executionStats").aggregate([{ $match: { createdAt: { $gte: ISODate("2025-01-01"), $lt: ISODate("2025-02-01") } } },{ $group: { _id: "$segment", revenue: { $sum: "$total" } } }], { allowDiskUse: true })'
{
  "stages": [
    { "$cursor": { "queryPlanner": { "winningPlan": { "stage": "IXSCAN" } }, "executionStats": { "totalDocsExamined": 4200000, "totalKeysExamined": 4200000 } } },
    { "$group": { "usedDisk": true, "spills": 3 } }
  ]
}

Значення: Групування витекло на диск (usedDisk: true). Це індикатор затримок і IO.

Рішення: Зменште кардинальність ключів групування, попередньо агрегуйте або перемістіть навантаження з MongoDB. Якщо диск-спілл стає звичним для ключових дашбордів — ви робите аналітику не в тому місці.

Завдання 8: MongoDB — перевірити, чи запит використовує запланований індекс

cr0x@server:~$ mongosh --quiet --eval 'db.orders.find({createdAt: {$gte: ISODate("2025-01-01"), $lt: ISODate("2025-02-01")}, status: "paid"}).sort({createdAt: -1}).hint({createdAt: 1}).explain("executionStats").queryPlanner.winningPlan'
{
  "stage": "FETCH",
  "inputStage": {
    "stage": "IXSCAN",
    "keyPattern": { "createdAt": 1 },
    "direction": "forward"
  }
}

Значення: Планувальник може використати індекс createdAt, але фільтр status не входить до індексу; можливо, буде перевірено більше документів, ніж потрібно.

Рішення: Додайте compound index на кшталт {status: 1, createdAt: 1}, якщо це топ-запит. Якщо ви ще й сортуєте по createdAt, уважно обирайте порядок полів в індексі.

Завдання 9: MongoDB — знайти поточні операції, що спричиняють проблему

cr0x@server:~$ mongosh --quiet --eval 'db.currentOp({ "active": true, "secs_running": { $gte: 5 } }).inprog.map(op => ({secs: op.secs_running, ns: op.ns, op: op.op, command: op.command && Object.keys(op.command)[0]}))'
[
  { "secs": 42, "ns": "prod.orders", "op": "command", "command": "aggregate" },
  { "secs": 19, "ns": "prod.events", "op": "command", "command": "find" }
]

Значення: Довготривалі операції включають агрегацію на orders. Це часто дашборд або експорт.

Рішення: Вбити або обмежити відому важку операцію, додати запобіжники (таймаути, maxTimeMS) і перемістити цей запит у репортингове сховище.

Завдання 10: ОС — перевірити затримку диска, коли аналітика «раптом сповільнюється»

cr0x@server:~$ iostat -x 1 3
avg-cpu:  %user %nice %system %iowait  %steal  %idle
          12.00  0.00   6.00    18.00    0.00  64.00

Device            r/s     w/s   rkB/s   wkB/s  await  svctm  %util
nvme0n1         820.0  1100.0  64000   72000   28.5   0.9   97.0

Значення: await високе і %util насичений. Зараз ваша база IO-bound, а не «повільний CPU».

Рішення: Зменште великі скани (індекси/партиціонування), перемістіть звітність на репліки з окремим сховищем або заплануйте важкі задачі у непіковий час. Не продовжуйте тонко налаштовувати синтаксис запитів, якщо диск повністю завантажений.

Завдання 11: ОС — підтвердити, чи тиск пам’яті викликає swap-шторми

cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 2  1  524288  10240  12000  98000   15   25  1800  2400  900 1200 20  8 50 22  0

Значення: Swap-in/out (si/so) ненульові під навантаженням. Це вбивця продуктивності бази, особливо для агрегацій.

Рішення: Перестаньте запускати звітність на вузлах з недостатньою пам’яттю. Додайте RAM, ізолюйте звітність або обмежте конкарентність. База, що свопиться — ввічливий спосіб сказати «я відмовляюся працювати».

Завдання 12: MySQL — виміряти насичення з’єднань і потоків

cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Threads_running'; SHOW GLOBAL STATUS LIKE 'Connections'; SHOW VARIABLES LIKE 'max_connections';"
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| Threads_running | 184   |
+-----------------+-------+
+---------------+----------+
| Variable_name | Value    |
+---------------+----------+
| Connections   | 9812231  |
+---------------+----------+
+-----------------+------+
| Variable_name   | Value|
+-----------------+------+
| max_connections | 500  |
+-----------------+------+

Значення: Багато паралельних потоків; ви ще не досягли max connections, але можливо має місце CPU context switching або внутрішні блокування.

Рішення: Додайте connection pooling, зменшіть паралелізм дашбордів або направте навантаження на репліку. Якщо трафік звітності викликає сплески потоків — ізолюйте його.

Завдання 13: MySQL — знайти найбільші таблиці і надмірність індексів (перевірка зберігання)

cr0x@server:~$ mysql -e "SELECT table_name, ROUND((data_length+index_length)/1024/1024/1024,2) AS size_gb FROM information_schema.tables WHERE table_schema='prod' ORDER BY (data_length+index_length) DESC LIMIT 10;"
+------------+---------+
| table_name | size_gb |
+------------+---------+
| events     | 420.55  |
| orders     | 88.12   |
| users      | 12.40   |
+------------+---------+

Значення: Якщо ваша найбільша таблиця також живить дашборди, ви скануєте багато даних, чи визнаєте ви це чи ні.

Рішення: Партиціонувати по даті, архівувати або перемістити events у сховище. Якщо «events» > 400 ГБ і використовується для інтерактивної аналітики — припиніть вдавати, що це просто OLTP.

Завдання 14: MongoDB — перевірити інвентар індексів і відсутність compound індексів

cr0x@server:~$ mongosh --quiet --eval 'db.orders.getIndexes().map(i => i.key)'
[
  { "_id": 1 },
  { "createdAt": 1 },
  { "customerId": 1 }
]

Значення: Існують лише одно-польові індекси. Агрегації з фільтрацією по кількох полях будуть переглядати надто багато документів.

Рішення: Додайте compound indexes, вирівняні під ваші топ-фільтри дашбордів (наприклад, {status: 1, createdAt: 1}). Якщо аналітика вимагає десятка таких індексів — це натяк, що варто ETL в SQL/warehouse.

Завдання 15: MySQL — перевірити binlog/реплікаційні налаштування для реплік звітності

cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'binlog_format'; SHOW VARIABLES LIKE 'sync_binlog';"
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| binlog_format | ROW   |
+---------------+-------+
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| sync_binlog   | 1     |
+---------------+-------+

Значення: Увімкнено надійний binlogging. Чудово для коректності, але це додає накладні записи. Ваша стратегія звітності не повинна покладатися на агресивні записи в пік.

Рішення: Якщо ETL для звітності б’є по первинному вузлу, перемістіть ETL поза первинним шляхом (CDC, стримінг або видобуток з реплік). Не «оптимізуйте» параметри надійності через повільність дашбордів.

Швидкий план діагностики: знайти вузьке місце за хвилини

Це план, який має висіти на стіні під час інциденту дашборда. У вас немає часу бути елегантним. У вас є час бути правильним.

Перший крок: визначте, чи справа в вартості запиту, конкурентності чи інфраструктурі

  1. Перевірте насичення: CPU, IO, пам’ять, мережа. Якщо IO зафіксований або відбувається свопінг, тонке налаштування індексів не врятує вас під час інциденту.
  2. Перевірте конкурентність: кількість виконуваних потоків/операцій. Звітність часто падає через забагато «розумних» запитів паралельно.
  3. Перевірте головних винуватців: slow query digest (MySQL) або currentOp/slow logs (MongoDB).

Другий крок: ізолюйте навантаження

  1. Направте дашборди на репліку для читання або на виділений reporting node, якщо він у вас є.
  2. Обмежте швидкість експортів і endpoint-ів «download CSV» (вони — приховані пакетні задачі).
  3. Увімкніть кешування для топових графіків, якщо точність допускає невелику застарілість.

Третій крок: доведіть план і виправте найефективніший елемент

  • MySQL: запустіть EXPLAIN, додайте відсутній складений індекс або перепишіть запит, щоб зменшити кількість рядків для сканування. Якщо групування величезне — побудуйте rollup table.
  • MongoDB: запустіть explain("executionStats") для pipeline-а, подивіться, скільки документів перевіряється і чи є disk spills. Якщо відбувається спілл і кардинальність висока — зупиніться і відвантажте.

Четвертий крок: запобігти повторенню

  • Створіть «бюджет запитів для дашбордів» (макс. час виконання, макс. скановані документи, макс. паралельність).
  • Додайте план регулярного перегляду повільних запитів.
  • Визначайте спільні метрики у view/таблицях, а не у фрагментах коду застосунку.

Типові помилки: симптом → корінь → виправлення

1) Симптом: дашборди щоразу повільнішають без змін коду

Корінь: ріст даних + відсутність партиціонування/архівування; запити з часом сканують ширші діапазони.

Виправлення: партиціонувати fact таблиці за датою (MySQL), будувати summary tables або ETL у сховище. В MongoDB вводьте time-bucket коллекції або відвантажуйте аналітику.

2) Симптом: «той самий запит» швидкий іноді і жахливий іноді

Корінь: чутливість до параметрів і спектр даних. Один клієнт/сегмент має у 100× більше даних; оптимізатор обирає план, що годиться для малих сегментів, але катастрофічний для великих.

Виправлення: у MySQL додати складені індекси під селективні предикати; переписати запит; підтримувати актуальну статистику. В MongoDB переконатися, що початковий $match селективний і індексований; уникати high-cardinality $group без попередньої агрегації.

3) Симптом: звітність «працює», але числа не збігаються між командами

Корінь: визначення метрик реалізовані ad-hoc у pipeline-ах або коді застосунку; немає спільного семантичного шару.

Виправлення: визначайте метрики централізовано (SQL view/контрольовані трансформації), версіонуйте зміни і тестуйте на фіксованих наборах даних. Ставте визначення метрик на рівень контракту API.

4) Симптом: кластер MongoDB стає нестабільним під час бізнес-рев’ю

Корінь: великі агрегації з disk spills; $lookup; недостатні індекси; ключ шардингу оптимізований для записів, не для звітності.

Виправлення: перемістіть аналітику в SQL/warehouse; попередньо агрегуйте; редизайньте ключ шардингу для аналітичних запитів лише якщо приймаєте OLTP-трейд-офи. Більшість команд не повинні воювати з цим у MongoDB.

5) Симптом: MySQL primary сповільнюється, коли аналітики запускають запити

Корінь: звітність виконується на primary; великі читання і тимчасові таблиці витісняють кеш OLTP; блокування/контенція.

Виправлення: відокремте звітність через репліки; застосуйте read-only користувачів; обмежте час запитів; використовуйте summary tables. Захищайте primary, він платить вашу зарплату (так і є).

6) Симптом: spike лагу реплік під час ETL або «оновлення метрик»

Корінь: великі пакети записів, великі транзакції або ETL, що оновлює багато рядків; репліка не встигає застосувати зміни.

Виправлення: інкрементальні оновлення, chunking, идемпотентні завантаження і планування. Розгляньте CDC у окреме репортингове сховище замість того, щоб бити MySQL масовими оновленнями.

7) Симптом: додавання індексів «нічого не дало»

Корінь: неправильний порядок полів в індексі, низька селективність, функції над стовпцями (non-sargable предикати), або справжній вузький елемент — на групуванні/сортуванні.

Виправлення: інспектуйте плани; уникайте обгортання індексованих колонок у функції; додавайте покриваючі/складені індекси; або попередньо агрегуйте. Індекси не магія; це математика з наслідками.

Чеклісти / покроковий план

Чекліст рішення: чи має звітність виконуватися напряму на MongoDB?

  • Більшість звітів — одноколекційні з селективними предикатами? Якщо так — можливо.
  • Потрібні часті крос-ентиті з’єднання, змінні визначення або вимірне моделювання? Якщо так — плануйте SQL/warehouse.
  • Потрібна сумісність з BI-інструментами з мінімальними зусиллями? SQL виграє.
  • Побачили disk spills агрегацій або тиск пам’яті? Це ваш знак виходу.

Покроковий план: побудувати адекватний шлях для звітності без драми

  1. Інвентаризація топових дашбордів: перелічіть топ-20 запитів за частотою і бізнес-важливістю.
  2. Класифікуйте кожний запит: OLTP-типу (точкові луки), аналітичний (скан/групування), експорт/батч (великі дампи).
  3. Виберіть стратегію ізоляції:
    • MySQL: читальні репліки для дашбордів, окрема репліка для експортів.
    • MongoDB: окремий аналітичний вузол лише за умови прийнятної eventual consistency; інакше — ETL назовні.
  4. Моделюйте дані явно:
    • Визначте факти (orders, events) та виміри (customer, product, campaign).
    • Вирішіть, які виміри потребують історії (slowly changing) vs знімків.
  5. Створіть мінімальний семантичний шар: SQL view для спільних визначень метрик і фільтрів.
  6. Додайте правильні індекси: на основі реальних запитів, а не інтуїції.
  7. Попередньо агрегуйте гарячі метрики: денні/годинні підсумки, інкрементально оновлювані.
  8. Встановіть запобіжники:
    • Таймаути запитів, ліміти паралельності, макс. розмір експорту.
    • Окремі ролі для аналітиків і сервісних облікових записів.
  9. Операціоналізуйте:
    • Каденс перегляду повільних запитів.
    • Алерти на лаг реплік і runbook-и.
    • Планування потужності з прив’язкою до використання дашбордів.
  10. Міграція поступово: почніть з одного дашборда, підтвердіть числа і потім масштабуйте.

Чекліст міграції: переміщення аналітики з MongoDB до MySQL (або SQL шару)

  • Визначте канонічну схему подій/замовлень (типи і nullable-поля).
  • Вирішіть idempotency-ключі для завантажень (наприклад, event_id).
  • Бекфілл історичних даних по чанках; валідовуйте підсумки по днях.
  • Запустіть dual-write або CDC-синхронізацію, доки не буде впевненості.
  • Заморозьте визначення у view; вимагається рев’ю для змін.
  • Переключайте дашборди по одному; тримайте старий пайплайн як rollback протягом вікна.

Питання та відповіді

1) Чи погана MongoDB для аналітики?

Ні. Вона хороша для аналітики, що відповідає її сильним сторонам: одноколекційні запити з хорошими індексами та помірною кардинальністю групувань. Вона гірша як універсальний BI-бекенд під довільні з’єднання та еволюційні бізнес-визначення.

2) Чому з’єднання такі важливі для звітності?

Тому що питання звітності реляційні: факти плюс виміри. Якщо ви уникаєте з’єднань, вкладючи все, ви обираєте модель версіонування і платите за це бекфіллами, дублюванням і неузгодженістю істини.

3) Хіба MongoDB не може просто використовувати $lookup і все буде добре?

$lookup — це з’єднання, але в моделі pipeline, яка може створювати великі проміжні результати і тиск на пам’ять. Для невеликих з’єднань це може працювати. Часто це стає крихким у масштабі, особливо при ad-hoc запитах.

4) Чому не запускати звітність на MySQL primary, якщо «достатньо швидко»?

Бо «достатньо швидко» — це брехня, яку ви говорите собі, поки не настає дедлайн ради ради ради. Навантаження звітності сплескове і непередбачуване, воно може витіснити гарячі сторінки OLTP, підвищити контенцію блокувань і спричинити каскадні затримки. Використовуйте репліки або окреме репортингове сховище.

5) Який найшвидший виграш для продуктивності звітності MySQL?

Зазвичай: додати правильний складений індекс для топового патерну дашборду і направити трафік звітності на читальну репліку. Другий найшвидший виграш — summary table для гарячої метрики.

6) Який найшвидший виграш для продуктивності MongoDB у звітності?

Зробіть перший $match селективним і індексованим; додайте compound indexes, що відповідають патернам фільтрації; використайте allowDiskUse, якщо це доцільно. Якщо запит все ще спіллиться і сканує мільйони документів — припиніть оптимізувати і відвантажте.

7) Чи варто зберігати аналітичні події в MongoDB взагалі?

Якщо ваш випадок використання подій — орієнтований на застосунок («показати стрічку активності користувача»), MongoDB може бути придатною. Для BI-рівня подій вам, швидше за все, знадобиться система, оптимізована для сканів, агрегацій і стиснення. Багато команд тримають MongoDB як джерело і реплікують події назовні.

8) Як зберегти узгодженість чисел звітності між командами?

Визначайте метрики централізовано (SQL view / контрольовані трансформації), версіонуйте зміни і тестуйте на фіксованих наборах даних. Уникайте логіки метрик, розкиданої по застосунковому коді або одноразових pipeline-ах.

9) Чи вистачить MySQL, або потрібен склад?

Якщо ваша звітність операційна (останні часові вікна, помірний обсяг даних, інтерактивні дашборди), MySQL з репліками і summary tables може вистачити. Якщо ви робите глибоку історію, широкі скани і важкі багатовимірні зрізи — склад значно зменшить біль.

10) Що, якщо команда вже знає MongoDB, а не SQL?

Вивчіть достатньо SQL, щоб оперувати звітністю розумно. Звітність — це інтерфейс бізнесу, а не інженерна вподобайка. SQL-грамотність платить рахунки.

Висновок: прагматичні наступні кроки

Якщо ви обираєте між MySQL і MongoDB саме для звітності та аналітики, обирайте SQL, якщо ви не можете чітко довести, що ваші звітні запити матимуть форму документів і так залишаться. Вони не залишаться. Організація передумає. Вона завжди так робить.

Зробіть наступне:

  1. Відведіть звітність від первинного шляху бази: репліки, кластер для звітів або окреме SQL-сховище.
  2. Запишіть топ-20 запитів звітності і зробіть їх першокласними: індекси, плани і бюджети.
  3. Припиніть вкладати змінні виміри у незмінні факти, якщо ви свідомо не хочете «as-written truth». Моделюйте історію цілеспрямовано.
  4. Побудуйте невеликий семантичний шар (view/таблиці), щоб компанія могла сперечатися про визначення без суперечок про pipeline-и.
  5. Операціоналізуйте нудні звички: перегляд повільних запитів, алерти на лаг реплік і навантажувальне тестування на даних, схожих на продакшн.

Команди «повертаються до SQL», бо SQL старіший, так — але головним чином тому, що він операційно зрозумілий під тиском. Коли звітність стає бізнес-критичною, зрозумілість перемагає хитромудрість. Кожного разу.

← Попередня
Збої SPF електронної пошти: 5 помилок у записах, що руйнують доставку (і їх виправлення)
Наступна →
Сучасний CSS‑reset для 2026: мінімальний, безпечний для форм і медіа

Залишити коментар