NoSQL
NoSQL — парасолькова назва баз даних, які не зберігають реляційних таблиць із запитами SQL: сховищ «ключ — значення», документних, ширококолонкових і графових. Більшість із них кладе кожен запис на вузол за ключем, який обираєте ви, і цей ключ вирішує, які запити дешеві, які дорогі, а які неможливі. Сторінка починається з шістнадцяти речей, які реляційна база робила за вас, і того, що робити натомість; далі — чотири сімейства, вибір ключа партиціювання, репліки й конфлікти, моделювання під патерни доступу, експлуатація, п’ять задокументованих кейсів і де все це вироджується.
- Чотири сімейства
- Ключ — значення, документні, ширококолонкові, графові
- Розміщення
- За ключем, який обираєте ви і який дешево не зміниш
- Поточні версії
- MongoDB 9.0 (вересень 2026); Cassandra 5.0 (6.0 — альфа); Valkey 9.1
Шістнадцять речей, які реляційна база робила за вас, — і що робити натомість
Ніхто не бере NoSQL-сховище, щоб утратити можливості. Його беруть заради масштабу, гнучкої форми запису або тому, що керований сервіс вмикається одним кліком. А потім по одній відкривають речі, які реляційна база робила, не чекаючи прохання. Ось шістнадцять таких речей: що робила реляційна база, що натомість робить сховище з партиціями і який патерн це повертає. Не кожне сховище втрачає всі шістнадцять — MongoDB має транзакції на кілька документів із 2018 року, — але кожне робить частину з них дорожчими. Наступні розділи посилаються на рядки за номерами.
| # | Реляційна база робила це | Сховище з партиціями натомість | Що робити натомість |
|---|---|---|---|
| 1 | Відповідала на питання, про яке ніхто не думав, коли проєктували таблицю | Запит без ключа партиціювання питає кожен вузол — або його відхиляють | Спершу виписати патерни доступу; обрати ключ так, щоб важливі запити його несли |
| 2 | З’єднувала таблиці під час читання | Жодних з’єднань між партиціями; застосунок робить ще один запит на кожен пов’язаний елемент | Зберігати відповідь у тій формі, яку читає екран; решту діставати одним пакетом |
| 3 | Знаходила рядки за будь-якою проіндексованою колонкою, і індекс ніколи не відставав | Індекс, що охоплює партиції, оновлюється асинхронно й може не бачити щойно зробленого запису | Глобальні індекси — для шляхів, що терплять відставання; де важливо — читати за ключем |
| 4 | Рахувала, підсумовувала й групувала: COUNT, SUM, GROUP BY | Агрегат сканує все — або його немає взагалі | Вести лічильники під час запису; решту віддавати в аналітичне сховище |
| 5 | Сортувала й ділила на сторінки за будь-якою колонкою | Порядок існує лише всередині однієї партиції — за її ключем сортування | Закласти потрібний порядок у ключ сортування; гортати від останнього побаченого ключа |
| 6 | Змінювала кілька рядків в одній транзакції | Атомарність на один елемент; транзакції на кілька елементів, де вони є, дорожчі й мають ліміти | Тримати те, що змінюється разом, в одному елементі; для решти — транзакція рушія в межах її лімітів |
| 7 | Тримала будь-яку колонку унікальною — один акаунт на email | Унікальність лише на первинному ключі | Другий елемент із ключем, що дорівнює унікальному значенню, записаний у тій самій транзакції |
| 8 | Не приймала посилання на те, чого не існує | Посилань не перевіряє ніщо | Тримати дочірнє всередині батьківського — або перевіряти в коді й терпіти «висячі» посилання під час читання |
| 9 | Одразу показувала вам ваш власний запис | Репліки відстають; читання може повернути значення з-перед вашого запису | Строго узгоджене або кворумне читання там, де це важливо |
| 10 | Вишиковувала паралельні оновлення одного рядка блокуваннями | Паралельні записи зводять потім — часто «останній запис перемагає», мовчки | Умовні записи з перевіркою версії — або типи даних, що зливаються |
| 11 | Не приймала рядок, що не відповідав схемі | Приймається будь-яка форма; за рік співіснують три форми | Поле версії в кожному записі, валідація під час запису |
| 12 | Змінювала колонку в кожному рядку одним виразом | Жоден вираз не переписує кожен запис | Відновлюване дозаповнення з обмеженою швидкістю — і читачі, що приймають обидві форми, доки воно не скінчиться |
| 13 | Працювала на одній машині, де жоден окремий ключ не міг бути гарячим | Увесь трафік одного ключа падає на одну партицію, а в неї фіксована стеля | Ключ із багатьма значеннями, що вживаються приблизно порівну; розщепити гарячий ключ; кешувати найгарячіші читання |
| 14 | Дозволяла рядкам одного клієнта рости без межі | Елемент чи партиція мають практичну стелю | Обмежити все, що росте: ділити на кошики за часом, виносити довгі списки в окремі елементи |
| 15 | Видаляла рядок — і читання не сповільнювалися | Видалення пише надгробок (tombstone), який читання мусять пропускати, доки його не прибере компакція | Прибирати цілі часові кошики чи таблиці, а не видаляти рядки по одному |
| 16 | Була базою, яку всі вже вміли захищати, бекапити й оновлювати | Другий рушій зі своїми типовими налаштуваннями, бекапами, оновленнями й ліцензією | Названий власник до першого запису; прочитати типові налаштування й ліцензію; відрепетирувати відновлення |
Чотири сімейства, один механізм і питання, на яке кожне не відповідає
Назва каже, чим ці бази не є, тож не називає нічого. Карло Строцці вжив її 1998 року для реляційної бази, яка просто не мала SQL-інтерфейсу. Значення, що прижилося, походить від 11 червня 2009 року: тоді Йохан Оскарссон зібрав у Сан-Франциско зустріч про відкриті розподілені нереляційні бази, і йому потрібен був короткий хештег. Ерік Еванс запропонував NoSQL. Пізніше галузь переосмислила це як «not only SQL» — «не лише SQL». За міткою стоять чотири моделі даних, що майже не мають нічого спільного, і обрати не те сімейство дорожче, ніж обрати не той продукт усередині сімейства.
| Сімейство | Що зберігає | Питання, на яке відповідає | Приклади | На що не відповідає |
|---|---|---|---|---|
| Ключ — значення | Значення за ключем; сховище не зазирає всередину | «Сесія за цим токеном» — значно швидше за мілісекунду | Redis, Valkey, Memcached; DynamoDB, якщо вживати її так | Будь-що про вміст значень |
| Документне | Вкладений запис з індексами на його полях | «Це замовлення разом з усім, що в ньому» | MongoDB, Firestore, Couchbase, Cosmos DB | Зв’язки між документами; зміни кількох документів коштують додатково |
| Ширококолонкове | Рядки, згруповані за ключем партиціювання й упорядковані всередині нього | «Останні 50 повідомлень у цьому каналі» — за мільйонів записів на секунду | Cassandra, ScyllaDB, Bigtable, HBase | Будь-що, що не починається з ключа партиціювання |
| Графове | Вузли й ребра — обидва повноцінні, обидва з властивостями | «Друзі друзів друзів»; найкоротший шлях між двома акаунтами | Neo4j, Amazon Neptune | Масові сканування й підсумки по всьому; поділ між машинами складний |
Під ту саму парасольку часто зараховують трьох сусідів. Пошукові рушії на кшталт Elasticsearch і OpenSearch відповідають на повнотекстові й фасетні запити. Сховища часових рядів відповідають на «ця метрика за це вікно». Векторні бази відповідають на «що найсхожіше на це» і мають окрему сторінку. Усі три зазвичай живляться від основної бази, а не є нею. Межі до того ж розмиваються з обох боків. PostgreSQL зберігає й індексує JSONJavaScript Object Notation — текстовий формат данихТекстовий формат для передавання даних: пари «ключ — значення», списки, числа й рядки. Він народився в JavaScript, але сьогодні його читає будь-яка мова — і саме тому ним відповідає більшість API, а не чимось потужнішим.Усі терміни як jsonb з версії 9.4 2014 року. MongoDB має транзакції з 2018-го, а Cassandra 6.0, яка 2026 року ще в альфі, приносить ACIDAtomicity, Consistency, Isolation, Durability — чотири обіцянки транзакціїЧотири гарантії транзакції в базі даних: вона стається вся або не стається зовсім, не лишає порушеним жодного оголошеного правила, паралельні транзакції не бачать одна одну недоробленою, а підтверджений коміт переживає збій. Ізоляція має налаштування, і типовий рівень у більшості рушіїв слабший, ніж обіцяє назва.Докладніше в статті →Усі терміни-транзакції на кілька партицій. У квітні 2024 року графи отримали ISO/IEC 39075, мову запитів GQL, — першу нову мову баз даних ISO після SQL. Тож сімейство описує, що сховище робить типово й дешево, а не межу того, що його можна змусити робити.
Перші три сімейства об’єднує механізм, що дає їм вирости за межі однієї машини, і його варто назвати прямо, бо він же є й обмеженням. Кожен запис призначається вузлу через хешування ключа, який ви обрали, проєктуючи таблицю. Запит, що несе цей ключ, іде рівно до одного вузла й лишається швидким, хоч би скільки даних було загалом. Запит, що його не несе, мусить питати кожен вузол і зливати відповіді — або, в кількох рушіях, його просто відхиляють. DynamoDB просканує цілу таблицю, якщо попросити, і виставить рахунок за кожен прочитаний елемент. Cassandra відхиляє такий запит, доки ви не допишете ALLOW FILTERING, — назву обрано так, щоб ви завагалися.
Той самий ключ вирішує й друге: чи розподіляється навантаження взагалі. Хешування рівномірно розкидає ключі лише тоді, коли самі ключі різноманітні й уживаються приблизно порівну. Партиціюйте за країною — і вузол із вашим найбільшим ринком візьме більшість трафіку, поки решта простоює. Партиціюйте за днем — і кожен запис у системі падатиме на один вузол до півночі. Гаряча партиція не оголошує себе гарячою партицією. Вона виглядає як повільність усього кластера, і додавання вузлів не допомагає, бо роботу ніколи не було поділено. Це рядок 13, і наступний розділ — про те, як його уникнути.
Ключ партиціювання — єдине рішення, яке не скасуєш
Ключ у цих сховищах зазвичай має дві частини, і вони роблять різну роботу. Ключ партиціювання вирішує, який вузол тримає запис. Ключ сортування — Cassandra називає його ключем кластеризації — вирішує порядок записів усередині цієї партиції. DynamoDB називає цю пару складеним первинним ключем. Cassandra пише його як PRIMARY KEY ((channel_id, bucket), message_id), де внутрішні дужки тримають ключ партиціювання. Bigtable і HBase не хешують ключ, а тримають рядки впорядкованими за всім ключем: діапазонні сканування від цього дешеві, але коли ключі лише зростають, кожен новий рядок потрапляє в кінець. MongoDB дає обрати одне з двох: хешований ключ шардування або діапазонний.
| Добрий ключ партиціювання… | Чому | Ключ, що цього не витримує |
|---|---|---|
| є в усіх важливих запитах | Запит без нього зачіпає кожен вузол (рядок 1) | Id замовлення, коли кожен екран починається з клієнта |
| має багато різних значень | Мало значень — це мало партицій, хоч би скільки вузлів ви оплачували | Країна, статус, тип тарифу |
| уживається приблизно порівну по всіх значеннях | Одне завантажене значення кладе все своє навантаження на одну партицію (рядок 13) | Сьогоднішня дата; акаунт знаменитості; один величезний клієнт |
| тримає кожну партицію обмеженою за розміром | Партиція, що росте без кінця, сповільнює компакцію, відновлення реплік і читання (рядок 14) | Сам лише id каналу; id датчика з усіма показами з 2019 року |
| не лише зростає — у сховищі, що тримає ключі впорядкованими | Кожен новий запис потрапляє в останній діапазон | Мітка часу на початку ключа рядка в Bigtable чи HBase |
Таблиця повідомлень Discord показує всі п’ять правил одразу. 2017 року команда перейшла з MongoDB на Cassandra. Їхній патерн доступу був майже завжди однаковий: прочитати найновіші повідомлення в одному каналі. Отже, канал мав бути в ключі партиціювання. Але сам лише канал дав би партицію, що росте без кінця, а завантажений канал набирає роки повідомлень. Тож до ключа партиціювання додали часовий кошик — приблизно десять днів повідомлень на кошик, — і це тримало партиції під 100 МБ, які команда встановила собі як межу, набагато нижче за 2 ГБ, що їх тоді дозволяла Cassandra. Ключем сортування став id повідомлення — Snowflake id, що сортується за часом створення, тож «найновіші п’ятдесят» — це одне коротке читання з кінця однієї партиції.
Кошики мають ціну, і платять її тихі канали. Каналу з трьома повідомленнями на місяць, щоб знайти п’ятдесят повідомлень, може знадобитися пройти назад дюжиною порожніх кошиків, тож код читання мусить знати, коли зупинитися. Розмір кошика — компроміс між розміром найзавантаженішої партиції та кількістю читань для найтихішого каналу. До того ж кошики лагодять лише рядок 14. Рядок 13 вони не лагодять: канал на мільйон людей однаково шле всі свої читання в той один кошик, що тримає сьогоднішній день. Допис Discord 2023 року описує саме це, і виправленням став не новий ключ, а шар перед базою — він у розділі з кейсами.
Кожен рушій ставить стелю для однієї партиції, і DynamoDB свою публікує. Кожна партиція дає щонайбільше 3000 одиниць читання й 1000 одиниць запису на секунду. Одиниця читання — це одне строго узгоджене читання до 4 КБ або два узгоджені «зрештою»; одиниця запису — один запис до 1 КБ. Адаптивна ємність перекидає невикористану ємність на гарячу партицію, але не понад цю стелю. Коли одному ключу справді потрібно більше — скажімо, лічильнику голосів під час трансляції, — задокументований патерн — шардування запису. Допишіть до ключа суфікс від 0 до 9, щоб записи розійшлися по десяти партиціях, а коли потрібен підсумок, прочитайте всі десять і складіть.
Репліки, кворуми й запис, що тихо зникає
Ці сховища тримають кілька копій кожного запису на різних вузлах — саме це дає їм пережити смерть машини. Кожне з них мусить відповісти на два питання. Скільки копій мають підтвердити запис, перш ніж клієнтові скажуть про успіх? І скільки треба спитати, перш ніж читання поверне відповідь? Стаття Amazon про Dynamo 2007 року описала цей компроміс трьома числами, якими користуються й досі. N — кількість копій, W — скільки з них мають підтвердити запис, R — скільки питає читання. Коли R + W більше за N, кожне читання питає щонайменше одну копію, що взяла останній запис.
Ця арифметика має дві пастки. По-перше, вона тримається, лише поки питають ті самі N вузлів. Сховища на кшталт Dynamo вживають «нестрогий» кворум: коли репліка лежить, запис приймає вузол-заступник і передає його пізніше, тож під час збою R + W > N перестає гарантувати обіцяне. По-друге, кворум каже, яке значення побачить читання; він нічого не каже про два записи того самого запису, що обидва вдалися. Це проблема конфліктів, і нижче для неї окрема діаграма.
Відома формула — узгодженість, доступність, стійкість до розділення мережі, оберіть дві — була гіпотезою Еріка Брюера 2000 року, яку Сет Гілберт і Ненсі Лінч довели 2002-го. Вона правдива й майже некорисна в щоденній роботі, бо мережа розділяється рідко. PACELC Деніела Абаді (2012) додає нормальний випадок: якщо мережу розділено — обирайте доступність або узгодженість; інакше — затримку або узгодженість. Це «інакше» — і є компроміс, з яким ви живете. Дочекатися більшості реплік повільніше, ніж спитати найближчу, і ця різниця сплачується на кожному читанні, а не лише під час інциденту.
| Рушій | Типово | Сильніший варіант | Чого коштує |
|---|---|---|---|
| DynamoDB | Читання, узгоджені «зрештою» | Строго узгоджене читання таблиці чи локального індексу; транзакції до 100 елементів | Удвічі більше одиниць читання; на глобальному індексі недоступне ніколи |
| Cassandra | Рівень узгодженості на кожен запит; багато драйверів типово питають одну репліку | QUORUM або LOCAL_QUORUM; легкі транзакції (IF NOT EXISTS) на Paxos; транзакції Accord у 6.0 | Затримка на кожному виклику; кілька обмінів повідомленнями на легку транзакцію |
| MongoDB | Write concern majority з версії 5.0; read concern local | Read concern majority або linearizable; причинно узгоджені сесії; транзакції | Затримка; транзакції мають власні ліміти й повтори |
| Кластер Redis / Valkey | Асинхронна реплікація | WAIT на певну кількість реплік | Зменшує, але не прибирає записи, які може загубити перемикання |
Важить те, що бачить користувач, а не назва рівня, тож називайте потрібну гарантію словами користувача. Узгодженість «зрештою» означає, що зміна може бути відсутньою на наступному екрані, а потім з’явитися, і двоє людей можуть на мить бачити різні значення. «Читай свої записи» означає, що ви завжди бачите власні зміни, тоді як чужі можуть відставати, — це рядок 9, і звичайні ліки — читати власні записи звідти, де ви їх зробили. Монотонні читання означають, що ви ніколи не побачите старішого значення після новішого, — саме це ламається, коли послідовні запити потрапляють на різні репліки. Кворумне читання відображає останній підтверджений запис ціною затримки й відмови, коли відповідає замало реплік.
Задокументована гарантія — це твердження, а твердження перевіряють. У травні 2020 року Jepsen проаналізував MongoDB 4.2.6 і з’ясував, що «навіть на найсильніших рівнях read concern і write concern вона не зберігала ізоляції знімків»: транзакції бачили частину, але не всі наслідки попередньої транзакції, а повтори записували те саме значення двічі. Типові налаштування для транзакцій були ще слабшими — read concern local і запис, підтверджений одним вузлом. Відтоді MongoDB зробила majority типовим write concern — з версії 5.0 2021 року. Урок не про одного постачальника. Читайте незалежний аналіз рушія, який обираєте, і тестуйте власні сценарії збоїв, бо налаштовану гарантію ви отримуєте, лише якщо типові налаштування й документація кажуть те саме.
Два записи — один уцілілий
Рядок 10 — тихий. Коли дві репліки прийняли зміну того самого запису, а потім мають звести їх докупи, типова поведінка — лишити версію з пізнішою міткою часу, а другу викинути. Cassandra робить так для кожного значення колонки; глобальні таблиці DynamoDB — між регіонами. Це не злиття, а тиха втрата, і у вашому застосунку вона не проходить жодним шляхом обробки помилок. Ще й залежить від того, чи збігаються годинники на машинах, а точно вони не збігаються ніколи. 2013 року Jepsen прогнав Riak із «останній запис перемагає» крізь розділення мережі й загубив 91% записів, які той підтвердив. Зі строгими кворумами на кожне читання й запис втрата була 92%, бо кворум вирішує, яке значення прочитають, а не чи зіллються два записи. З типом даних, створеним для злиття, не загубилося нічого.
Тож вирішіть для кожного типу запису, що має статися, коли два записи зіткнулися. Справжніх відповідей три. Умовний запис змушує другого автора гучно впасти: кожен тутешній рушій уміє записати «лише якщо версія досі 4» — умовний вираз у DynamoDB, IF version = 4 у Cassandra, фільтр за версією в MongoDB, — а той, хто програв, читає знову й повторює. Тип даних, що зливається, зберігає обидва: лічильники, що додаються, множини, що об’єднуються, — сімейство, відоме як CRDT, conflict-free replicated data types, безконфліктні репліковані типи даних. А дописування зберігає обидва, бо ніколи нічого не перезаписує: кожну зміну зберігають як окремий елемент, а поточний стан обчислюють зі списку. Лишити це на типову поведінку — значить обрати втрату тієї правки, про яку ви не подумали.
Моделюйте під запит, а не під сутність
Реляційне моделювання зберігає кожен факт один раз і збирає відповіді з’єднаннями під час читання. Без з’єднань (рядок 2) це збирання доводиться робити натомість на етапі проєктування. Ви зберігаєте відповідь у тій формі, якої потребує екран, і погоджуєтеся зберігати деякі факти більше ніж один раз. Документ замовлення, що несе ім’я й адресу клієнта поруч із позиціями, — не провал моделювання, а сама модель. Ціна в тому, що коли клієнт змінить ім’я, змінитися мають кілька документів, і знає, які саме, лише ваш код.
Метод — перелік до схеми. Запишіть кожен патерн доступу, потрібний продуктові, як речення, а тоді дайте кожному ключ. Ось невеликий чат-продукт у стилі, який власні настанови DynamoDB називають дизайном з однією таблицею: кілька видів елементів ділять одну таблицю, а розрізняють їх за префіксами ключів:
| Патерн доступу | Ключ партиціювання | Ключ сортування | Примітка |
|---|---|---|---|
| Найновіші 50 повідомлень у каналі | CH#123#2026-10-01 | MSG#<id>, від нових | Десятиденний кошик у ключі (рядок 14) |
| Відредагувати одне повідомлення | CH#123#2026-10-01 | MSG#<id> | Клієнт мусить знати канал і кошик — тому id кодує час |
| Канали, до яких належить користувач | USER#42 | CH#<id> | Один елемент членства на пару |
| Учасники каналу | CH#123 (в індексі) | USER#<id> | Ті самі елементи, прочитані через глобальний індекс із переставленими ключами, — і з відставанням (рядок 3) |
| Кількість непрочитаних на користувача й канал | USER#42 | UNREAD#CH#<id> | Лічильник, що ведеться під час запису, а не підрахунок під час читання (рядок 4) |
| Пошук повідомлень за текстом | — | — | Не в цьому сховищі: пошуковий індекс, що живиться з потоку змін |
Прочитайте таблицю на те, чого в ній немає. Патерну «усі повідомлення цього користувача» немає, тож жоден ключ його не обслуговує, а додати його згодом — означає записати кожне повідомлення вдруге під ключем користувача. Елемент членства фактично існує двічі — раз під користувачем, раз через індекс під каналом, — і індекс оновлюється асинхронно. А останній патерн чесно відправлено деінде. Команда, що складає такий перелік насправді, зазвичай знаходить один-два патерни, що не лягають у жоден ключ, — і це найкорисніша знахідка всієї вправи.
| Вкладайте пов’язані дані, коли… | Посилайтеся на них, коли… |
|---|---|
| їх читають разом із батьківським майже щоразу | їх часто читають окремо |
| дочірніх мало й кількість обмежена — адреси, позиції замовлення | список може рости без межі — коментарі, підписники, події (рядок 14) |
| вони належать лише одному батьківському | їх ділять багато батьківських — товар, профіль користувача |
| вони мають змінюватися атомарно разом із батьківським (рядок 6) | вони змінюються за власним розкладом, іншими авторами |
Середній рядок другої колонки — місце, де документи ламаються. MongoDB обмежує один документ 16 МБ, DynamoDB — один елемент 400 КБ, а власна документація MongoDB називає «необмежені масиви» антипатерном. Допис, що вкладає свої коментарі, працює, доки один допис не стане вірусним. Соцмережі зустрічають той самий вибір у масштабі. Twitter описував, як кожен твіт записують у кешовану стрічку кожного підписника — розсилання під час запису, — крім акаунтів із дуже великою кількістю підписників: їхні твіти домішують, коли стрічку читають. Кожен денормалізований дизайн закінчується подібним правилом: дублюйте там, де переважають читання, і зупиняйтеся там, де один запис перетворився б на мільйони.
Вторинні індекси — місце, де маркетинг і механіка розходяться. Індекс, локальний для партиції, дешевий і узгоджений, бо живе поруч із даними, які індексує. Індекс, що охоплює партиції, — глобальний вторинний індекс DynamoDB, матеріалізоване подання Cassandra, — доводиться підтримувати між вузлами. Тож оновлюють його асинхронно, читання з нього може не побачити щойно записаного рядка, а кожен запис у таблицю стає ще й записом у кожен індекс. Це справжній інструмент для другорядних шляхів доступу й поганий замінник ключа, що пасує основному. А ще це окремі партиції, що їх кейс DynamoDB 2015 року показує з боку оператора.
Це правильна форма
- Патерни доступу відомі й вузькі: діставати цю річ за цим id знову й знову.
- Обсяг запису виміряно — він більший за одну машину, і ключ розподіляє його рівномірно.
- Записи самодостатні: те, що читають разом, можна зберігати разом.
- Коротке вікно застарілості прийнятне для людей, які користуються продуктом.
Це реляційна задача
- Приходять нові питання, яких ніхто не передбачав, і кожному потрібен інший шлях доступу.
- Кілька записів мають змінитися разом або не змінитися зовсім: гроші, залишки, бронювання.
- Той самий факт потрібен із багатьох боків, тож кожна копія множить способи, якими він розійдеться.
- Обсяг великий за відчуттям, а не за виміром, — а так буває здебільшого.
Експлуатація — дрейф схеми, видалення, ємність, типові налаштування й ліцензія
«Безсхемність» — слово, що коштує командам найдорожче (рядок 11). Схема є завжди; питання лише в тому, чи вона записана й забезпечена, чи мається на увазі кодом, який останнім торкався колекції. Через рік у документному сховищі лежать три покоління форми запису: десь поле є, десь його немає, десь воно містить інший тип. Далі кожен шлях читання мусить обробляти всі три, і ніщо не скаже, коли один пропущено. Команди, що лишаються при глузді, ставлять номер версії на кожен запис, валідують під час запису — MongoDB забезпечує валідатор $jsonSchema з версії 3.6, або це робить застосунок, — і сприймають зміну форми як міграцію з планом.
Самі міграції змінюють характер (рядок 12). Жоден вираз не перепише поле по всій колекції, тож зміна форми стає фоновою задачею, що обходить кожен запис. Вона мусить бути відновлюваною, обмеженою за швидкістю, щоб не заморити живий трафік, і безпечною для повторного запуску. Тим часом застосунок читає і стару, і нову форму. Тож міграція не завершена, коли завершилася задача. Вона завершена, коли видалено код, що розуміє стару форму, — і саме цей крок ніхто не планує. Лінива міграція, що оновлює запис, коли його вдруге читають, обходиться без задачі, але робить стару форму вічною для кожного запису, якого ніхто не читає.
Видалення — несподіванка рядка 15. У Cassandra та її родичах видалення не прибирає даних. Воно пише надгробок (tombstone) — позначку «це видалено о такій-то порі», яка мусить пережити кожну репліку, що ще може тримати старе значення. Типовий період очікування, gc_grace_seconds, — десять днів. Доки компакція не прибере надгробків, кожне читання цієї партиції мусить через них переступати: Cassandra попереджає після тисячі в одному читанні й обриває читання на ста тисячах. Тому черга, збудована на таблиці, — вставити, прочитати, видалити — працює то повільніше, що більше роботи завершила. Ліки — видаляти масово, у формах, які рушій обробляє дешево: прибирати цілі часові кошики або дати TTL, «час життя» на кожному записі, списувати дані з відомим терміном життя. TTL у DynamoDB прибирає прострочені елементи «зазвичай упродовж кількох днів» і безкоштовно, тож читання однаково мають відфільтровувати прострочене.
Ємність і вартість теж ідуть за ключем. На сервісі з оплатою за запит сканування виставляє рахунок за кожен прочитаний елемент, глобальний індекс — за кожен запис удруге, а гаряча партиція впирається у свою стелю й отримує відмови, поки таблиця загалом здебільшого простоює. Через це рахунок — корисний огляд дизайну: стаття витрат, що росте швидше за трафік, зазвичай вказує на сканування, індекс, якого ніхто не читає, або ключ, що мав бути іншим.
Рядок 16 — про все, що не є моделлю даних. Типові налаштування — частина продукту. MongoDB до версії 3.6 слухала на кожному мережевому інтерфейсі, а автентифікацію було вимкнено, доки хтось її не вмикав; 3.6, у листопаді 2017 року, зробила типовою прив’язку до localhost. Хвиля вимагачів із розділу кейсів сталася за місяці до цієї зміни. Перевірте, на чому слухає рушій і чи питає він пароль, до першого запису, а не після. Ліцензія — інше типове налаштування, бо кілька з цих рушіїв свою змінили:
| Рушій | Що змінилося | Відкрита альтернатива |
|---|---|---|
| MongoDB | AGPL → SSPL, жовтень 2018; OSI так і не визнала її відкритою ліцензією | Сумісні шари поверх PostgreSQL на кшталт FerretDB |
| Redis | BSD → RSALv2 або SSPLv1, березень 2024; AGPLv3 додано як третій варіант у Redis 8, травень 2025 | Valkey — форк під Linux Foundation з ліцензією BSD; 9.1 у травні 2026 |
| Elasticsearch | Apache 2.0 → SSPL або Elastic License, 2021; додано варіант AGPL, серпень 2024 | OpenSearch, Apache 2.0 |
| Cassandra | Без змін: Apache 2.0, під керуванням Apache Software Foundation | — |
Нарешті — шлях назад, бо він існує. 2018 року Guardian переніс базу за своєю CMS — близько 2,3 мільйона матеріалів, частину з яких створено двома десятиліттями раніше, — з MongoDB на PostgreSQL в Amazon RDS, зберігши документи в колонках jsonb. Причиною була не модель даних. Експлуатація кластера коштувала їм «щонайменше двох місяців інженерного часу на рік», а два збої зупиняли публікацію щонайменше на годину кожен. Проксі надсилав кожен запит в обидві бази й записував кожну розбіжність у відповідях, доки їх не лишилося, і за десять місяців від початку роботи старий кластер вимкнули. Сучасні реляційні рушії роблять більшість того, заради чого брали документне сховище. Беріть другий рушій, коли цього вимагає виміряна потреба — обсяг запису понад одну машину, обхід графа, кеш із бюджетом у мілісекунду, — і вмійте сказати, яка саме. «Нам, можливо, знадобиться масштабуватися» до них не належить.
П’ять кейсів і те, чого в кожному бракувало
Кожен із цих кейсів — це один-два рядки таблиці вгорі, зустрінуті там, де їх уже не можна було ігнорувати. Два розповіли ті, хто збудував системи, один — незалежний тестувальник, один — оператор, і один — з боку зловмисників.
| Кейс | Що сталося | Що це показує |
|---|---|---|
| Amazon Dynamo, 2007 | Кошик зливав версії, що розійшлися, тож жоден доданий товар не губився — а видалені могли повернутися | Правило злиття — продуктове рішення (рядок 10) |
| Riak під Jepsen, 2013 | «Останній запис перемагає» загубив 91% підтверджених записів під час розділення; строгі кворуми — 92%; множина, що зливається, — нуль | Кворум не лагодить перезаписів; злиття лагодить (рядки 9–10) |
| DynamoDB, 2015 | Сервери зберігання одночасно запросили свої списки партицій; списки, роздуті глобальними індексами, перевантажили сервіс метаданих; близько 55% помилок кілька годин | Індекс — це теж партиції (рядок 3) |
| Хвиля вимагачів у MongoDB, 2017 | Щонайменше 29 000 баз, відкритих в інтернет без пароля, стерли й вимагали викуп; ті, хто платив, нічого не отримали | Типові налаштування — частина продукту (рядок 16) |
| Discord, 2017–2023 | Часові кошики в ключі, гонитва, що лишала напівзаписані рядки, надгробки, через які канал вантажився 20 секунд, гарячі партиції під кворумними читаннями | Рядки 10, 13, 14 і 15 в одній системі — і ключ, що пережив дві зміни рушія |
Кейс DynamoDB варто показати кожному, хто вважає глобальний індекс безкоштовним. Рано в неділю 20 вересня 2015 року короткий збій мережі в регіоні US-East змусив багато серверів зберігання водночас запросити в сервісу метаданих своє «членство» — перелік партицій, які тримає кожен. Ці переліки різко зросли за попередні місяці, бо клієнти швидко взялися за глобальні вторинні індекси, і власний розбір AWS пояснює, чому це важило: глобальні індекси «мають власний набір партицій». Сервіс метаданих сповільнився під великими запитами й почав відповідати за таймаутом. Сервери, що не могли підтвердити своє членство, виводили себе з роботи й повторювали запит, додаючи навантаження. З 2:37 за тихоокеанським часом частка помилок трималася близько 55%; о 5:06 AWS призупинила запити, щоб сервіс відновився, і до 7:10 більшість трафіку повернулася. Виправлення — ємність, моніторинг розміру членства, довші таймаути й поділ сервісу метаданих на сегменти. Для клієнта висновок простіший: кожен індекс — це ще партиції, які хтось мусить відстежувати.
Хвилі вимагачів не знадобилося жодної вразливості. В останні дні 2016-го й перші дні 2017 року зловмисники шукали в інтернеті сервери MongoDB, що відповідали без пароля, копіювали або просто видаляли дані й лишали записку з вимогою біткоїнів. До 10 січня 2017 року дослідники, що відстежували атаки, нарахували щонайменше 29 000 стертих баз. Кілька груп працювали з тими самими серверами й перезаписували записки одна одної, тож жертва не могла знати, у кого — якщо взагалі в когось — є копія, і жоден із тих, хто заплатив, не повідомив, що отримав дані назад. Кожен із цих серверів працював так, як його налаштували. Бракувало контролю, якого ніхто не вважає частиною моделі даних: перевірити, на чому слухає база й чи питає вона облікові дані, — до того, як у ній з’явиться щось варте крадіжки.
Де це вироджується
NoSQL-сховище рідко підводить через те, що рушій не пасував до задачі. Воно підводить, бо задачу так і не записали. Майже кожен інцидент — одне з цього:
- Сімейство, обране через розповідь про масштаб, а не через форму даних і запитів.
- Ключ партиціювання, обраний до того, як виписали патерни доступу, — перший новий екран його й знецінює (рядок 1).
- Запити «розкидай і збери», додані по одному, доки кожне читання не почне зачіпати весь кластер.
- Ключ із малою кількістю значень — країна, день, статус, — що концентрує трафік на одній партиції (рядок 13).
- Список, вкладений у запис, якого ніхто не обмежив, — доки один запис не впреться в ліміт розміру (рядок 14).
- «Останній запис перемагає», лишений у продукті, де двоє клієнтів справді редагують той самий запис (рядок 10).
- З’єднання, перебудовані в коді застосунку, — та сама робота, тільки без планувальника й індексу (рядок 2).
- Ні поля версії, ні валідації, тож три форми запису співіснують непоміченими (рядок 11).
- Черга чи журнал, збудовані на видаленнях, що сповільнюються, коли накопичуються надгробки (рядок 15).
- Друга база, взята заради однієї функції й далі не експлуатована ніким конкретно (рядок 16).
Чесна межа в тому, що сховище з партиціями швидке рівно на тих питаннях, під які його спроєктували, і дороге на кожному іншому. Це не вада, яку виправить кращий продукт; це компроміс, яким оплачено масштаб. Команда, що знає свої питання, отримує передбачувану затримку за будь-якого розміру. Команда, що не знає, витратить цю перевагу на сканування, дублікати й другу базу для запитів, на які не відповідає перша. І жоден із рушіїв не вирішить, що має статися, коли двоє людей одночасно змінюють те саме. Це продуктове правило, і написати його можуть лише ті, хто володіє продуктом.
Практична перевірка займає пів дня. Випишіть кожен патерн доступу, який продукт має сьогодні, і ключ, що обслуговує кожен; будь-який патерн, що обслуговується скануванням, — знахідка. Знайдіть найбільшу партицію, найбільший елемент і найзавантаженіший ключ за останній тиждень. Знайдіть кожне місце, де два клієнти можуть записати той самий запис, і скажіть, що станеться, коли вони це зроблять. Перевірте, на чому слухає база, чи питає вона пароль і коли востаннє репетирували відновлення. І назвіть людину, яка нею володіє.
Коли це застосовувати
Застосовуйте, коли
- Відомий вузький набір патернів доступу на обсязі, що за вимірами перевищує одну машину: події, телеметрія, повідомлення.
- Записи, які читають і пишуть цілими й які ні від чого не залежать: сесії, кошики, стан пристрою.
- Кеші, лічильники й обмеження частоти, де сховище «ключ — значення» простіше й швидше за будь-яку альтернативу.
- Обхід зв’язків невідомої глибини — саме для цього й існує графовий рушій.
Уникайте, коли
- Усе, де кілька записів мають змінюватися разом, — якщо рушій не має транзакцій на кілька елементів, які ви перевірили.
- Звітність і довільний аналіз, де питання невідомі, а кожному потрібен інший шлях доступу.
- Продукт, що ще відкриває свої патерни доступу: ключ, який треба обрати першим, — саме те, що ще рухається.
- Обсяг, який ви припустили, а не виміряли, і який реляційна база витримала б спокійно.
Джерела й метод
На чому стоїть ця стаття
- 17 джерел
- 4 задокументовані збої
- Перевірено 13 жовтня 2026 р.
Збої, на яких вчиться стаття
Riak, DynamoDB, хвиля викупів MongoDB і Discord у «П’ять кейсів»- ДослідженняKyle Kingsbury — Jepsen: Riak (відкривається в новій вкладці)aphyr.com · 19.05.2013
- ПостмортемSummary of the Amazon DynamoDB Service Disruption and Related Impacts in the US-East Region (відкривається в новій вкладці)Amazon Web Services · 09.2015
- СтаттяBrian Krebs — Extortionists Wipe Thousands of Databases, Victims Who Pay Up Get Stiffed (відкривається в новій вкладці)Krebs on Security · 10.01.2017
- ПостмортемStanislav Vishnevskiy — How Discord Stores Billions of Messages (відкривається в новій вкладці)Discord · 13.01.2017
- ПостмортемBo Ingram — How Discord Stores Trillions of Messages (відкривається в новій вкладці)Discord · 06.03.2023
Дослідження
Dynamo, Bigtable і що CAP каже, а чого ні- ДослідженняGiuseppe DeCandia et al. — Dynamo: Amazon’s Highly Available Key-value Store (відкривається в новій вкладці)ACM SOSP 2007 · 2007
- ДослідженняFay Chang et al. — Bigtable: A Distributed Storage System for Structured Data (відкривається в новій вкладці)OSDI 2006 · Google Research · 2006
- ДослідженняSeth Gilbert, Nancy Lynch — Brewer’s conjecture and the feasibility of consistent, available, partition-tolerant web services (відкривається в новій вкладці)ACM SIGACT News · 2002
- ДослідженняEric Brewer — CAP twelve years later: How the “rules” have changed (відкривається в новій вкладці)IEEE Computer · 2012
- ДослідженняDaniel Abadi — Consistency Tradeoffs in Modern Distributed Database System Design (відкривається в новій вкладці)IEEE Computer · 2012
- ДослідженняJepsen: MongoDB 4.2.6 (відкривається в новій вкладці)jepsen.io · 15.05.2020
Практики й походження термінів
ключ партиції, межі кожного сховища і дві міграції- ДокументаціяBest practices for designing and using partition keys effectively in DynamoDB (відкривається в новій вкладці)AWS Documentation
- ДокументаціяMongoDB Database Manual (відкривається в новій вкладці)MongoDB Docs
- ДокументаціяApache Cassandra Documentation (відкривається в новій вкладці)Apache Software Foundation
- СтандартISO/IEC 39075:2024 — Database languages — GQL (відкривається в новій вкладці)ISO · 04.2024
- СтаттяRaffi Krikorian — Timelines at Scale (відкривається в новій вкладці)InfoQ · QCon 2012 · 2012
- СтаттяBye bye Mongo, Hello Postgres (відкривається в новій вкладці)The Guardian Engineering Blog · 30.11.2018
Дослідження з Claude. Пошук і звірку першоджерел зроблено з Claude (Anthropic). Відбір, текст і перевірку фактів — редакція Anatomy of IT Product.
Було корисно?
Поділіться з тим, хто працює над тією ж задачею.