РозробкаSenior

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 зберігає й індексує як jsonb з версії 9.4 2014 року. MongoDB має транзакції з 2018-го, а Cassandra 6.0, яка 2026 року ще в альфі, приносить -транзакції на кілька партицій. У квітні 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, що сортується за часом створення, тож «найновіші п’ятдесят» — це одне коротке читання з кінця однієї партиції.

Таблиця повідомлень Discord, як її описано в дописі 2017 року. Якби ключем партиціювання був сам канал, одна партиція тримала б усі повідомлення, які канал колись мав, і переросла б 100 МБ, що їх команда встановила собі як межу, — а великі партиції сповільнюють компакцію, відновлення реплік і читання. Десятиденний часовий кошик у ключі ділить ті самі повідомлення на партиції обмеженого розміру, а найчастіший запит — найновіші п’ятдесят повідомлень — читає лише найновіший кошик. Кошики коштують тихим каналам, яким доводиться йти назад порожніми кошиками, і не лагодять завантаженого каналу: усі його читання однаково йдуть в один кошик, що тримає сьогоднішній день.

Кошики мають ціну, і платять її тихі канали. Каналу з трьома повідомленнями на місяць, щоб знайти п’ятдесят повідомлень, може знадобитися пройти назад дюжиною порожніх кошиків, тож код читання мусить знати, коли зупинитися. Розмір кошика — компроміс між розміром найзавантаженішої партиції та кількістю читань для найтихішого каналу. До того ж кошики лагодять лише рядок 14. Рядок 13 вони не лагодять: канал на мільйон людей однаково шле всі свої читання в той один кошик, що тримає сьогоднішній день. Допис Discord 2023 року описує саме це, і виправленням став не новий ключ, а шар перед базою — він у розділі з кейсами.

Кожен рушій ставить стелю для однієї партиції, і DynamoDB свою публікує. Кожна партиція дає щонайбільше 3000 одиниць читання й 1000 одиниць запису на секунду. Одиниця читання — це одне строго узгоджене читання до 4 КБ або два узгоджені «зрештою»; одиниця запису — один запис до 1 КБ. Адаптивна ємність перекидає невикористану ємність на гарячу партицію, але не понад цю стелю. Коли одному ключу справді потрібно більше — скажімо, лічильнику голосів під час трансляції, — задокументований патерн — шардування запису. Допишіть до ключа суфікс від 0 до 9, щоб записи розійшлися по десяти партиціях, а коли потрібен підсумок, прочитайте всі десять і складіть.

Репліки, кворуми й запис, що тихо зникає

Ці сховища тримають кілька копій кожного запису на різних вузлах — саме це дає їм пережити смерть машини. Кожне з них мусить відповісти на два питання. Скільки копій мають підтвердити запис, перш ніж клієнтові скажуть про успіх? І скільки треба спитати, перш ніж читання поверне відповідь? Стаття Amazon про Dynamo 2007 року описала цей компроміс трьома числами, якими користуються й досі. N — кількість копій, W — скільки з них мають підтвердити запис, R — скільки питає читання. Коли R + W більше за N, кожне читання питає щонайменше одну копію, що взяла останній запис.

N — кількість копій, W — скільки з них мають підтвердити запис, R — скільки питає читання. Коли R + W більше за N, репліки, що взяли запис, і репліки, яких питає читання, мусять мати щонайменше одну спільну, тож читання бачить останнє підтверджене значення. З W = 1 і R = 1 із трьох читання може потрапити на копію, до якої запис ще не дійшов. Два застереження: під час збоїв сховища на кшталт Dynamo приймають записи на вузлах-заступниках — нестрогий кворум, — тож перетин більше не гарантовано; і кворум нічого не каже про два записи того самого запису, що обидва вдалися. Це конфлікт, і його вирішує правило злиття.

Ця арифметика має дві пастки. По-перше, вона тримається, лише поки питають ті самі N вузлів. Сховища на кшталт Dynamo вживають «нестрогий» кворум: коли репліка лежить, запис приймає вузол-заступник і передає його пізніше, тож під час збою R + W > N перестає гарантувати обіцяне. По-друге, кворум каже, яке значення побачить читання; він нічого не каже про два записи того самого запису, що обидва вдалися. Це проблема конфліктів, і нижче для неї окрема діаграма.

Відома формула — узгодженість, доступність, стійкість до розділення мережі, оберіть дві — була гіпотезою Еріка Брюера 2000 року, яку Сет Гілберт і Ненсі Лінч довели 2002-го. Вона правдива й майже некорисна в щоденній роботі, бо мережа розділяється рідко. PACELC Деніела Абаді (2012) додає нормальний випадок: якщо мережу розділено — обирайте доступність або узгодженість; інакше — затримку або узгодженість. Це «інакше» — і є компроміс, з яким ви живете. Дочекатися більшості реплік повільніше, ніж спитати найближчу, і ця різниця сплачується на кожному читанні, а не лише під час інциденту.

РушійТиповоСильніший варіантЧого коштує
DynamoDBЧитання, узгоджені «зрештою»Строго узгоджене читання таблиці чи локального індексу; транзакції до 100 елементівУдвічі більше одиниць читання; на глобальному індексі недоступне ніколи
CassandraРівень узгодженості на кожен запит; багато драйверів типово питають одну реплікуQUORUM або LOCAL_QUORUM; легкі транзакції (IF NOT EXISTS) на Paxos; транзакції Accord у 6.0Затримка на кожному виклику; кілька обмінів повідомленнями на легку транзакцію
MongoDBWrite concern majority з версії 5.0; read concern localRead 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%, бо кворум вирішує, яке значення прочитають, а не чи зіллються два записи. З типом даних, створеним для злиття, не загубилося нічого.

Два клієнти змінюють той самий кошик майже в ту саму мить на двох репліках. Кожен запис вдається й отримує підтвердження. Коли репліки зводять докупи, типова поведінка лишає версію з пізнішою міткою часу й мовчки викидає іншу — клієнтові B сказали, що його запис вдався, і ніхто ніколи не скаже інакше. Мітки часу беруться з власного годинника кожної машини, тож пізніша мітка навіть не гарантує пізнішого запису. Три альтернативи зберігають обидві правки: умовний запис змушує другого автора гучно впасти й повторити; тип даних, що зливається, як-от множина CRDT, зберігає обидва товари; а зберігання кожної зміни як окремого дописаного елемента означає, що ніщо ніколи не перезаписується.

Тож вирішіть для кожного типу запису, що має статися, коли два записи зіткнулися. Справжніх відповідей три. Умовний запис змушує другого автора гучно впасти: кожен тутешній рушій уміє записати «лише якщо версія досі 4» — умовний вираз у DynamoDB, IF version = 4 у Cassandra, фільтр за версією в MongoDB, — а той, хто програв, читає знову й повторює. Тип даних, що зливається, зберігає обидва: лічильники, що додаються, множини, що об’єднуються, — сімейство, відоме як CRDT, conflict-free replicated data types, безконфліктні репліковані типи даних. А дописування зберігає обидва, бо ніколи нічого не перезаписує: кожну зміну зберігають як окремий елемент, а поточний стан обчислюють зі списку. Лишити це на типову поведінку — значить обрати втрату тієї правки, про яку ви не подумали.

Моделюйте під запит, а не під сутність

Реляційне моделювання зберігає кожен факт один раз і збирає відповіді з’єднаннями під час читання. Без з’єднань (рядок 2) це збирання доводиться робити натомість на етапі проєктування. Ви зберігаєте відповідь у тій формі, якої потребує екран, і погоджуєтеся зберігати деякі факти більше ніж один раз. Документ замовлення, що несе ім’я й адресу клієнта поруч із позиціями, — не провал моделювання, а сама модель. Ціна в тому, що коли клієнт змінить ім’я, змінитися мають кілька документів, і знає, які саме, лише ваш код.

Метод — перелік до схеми. Запишіть кожен патерн доступу, потрібний продуктові, як речення, а тоді дайте кожному ключ. Ось невеликий чат-продукт у стилі, який власні настанови DynamoDB називають дизайном з однією таблицею: кілька видів елементів ділять одну таблицю, а розрізняють їх за префіксами ключів:

Патерн доступуКлюч партиціюванняКлюч сортуванняПримітка
Найновіші 50 повідомлень у каналіCH#123#2026-10-01MSG#<id>, від новихДесятиденний кошик у ключі (рядок 14)
Відредагувати одне повідомленняCH#123#2026-10-01MSG#<id>Клієнт мусить знати канал і кошик — тому id кодує час
Канали, до яких належить користувачUSER#42CH#<id>Один елемент членства на пару
Учасники каналуCH#123 (в індексі)USER#<id>Ті самі елементи, прочитані через глобальний індекс із переставленими ключами, — і з відставанням (рядок 3)
Кількість непрочитаних на користувача й каналUSER#42UNREAD#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. Хвиля вимагачів із розділу кейсів сталася за місяці до цієї зміни. Перевірте, на чому слухає рушій і чи питає він пароль, до першого запису, а не після. Ліцензія — інше типове налаштування, бо кілька з цих рушіїв свою змінили:

РушійЩо змінилосяВідкрита альтернатива
MongoDBAGPL → SSPL, жовтень 2018; OSI так і не визнала її відкритою ліцензієюСумісні шари поверх PostgreSQL на кшталт FerretDB
RedisBSD → RSALv2 або SSPLv1, березень 2024; AGPLv3 додано як третій варіант у Redis 8, травень 2025Valkey — форк під Linux Foundation з ліцензією BSD; 9.1 у травні 2026
ElasticsearchApache 2.0 → SSPL або Elastic License, 2021; додано варіант AGPL, серпень 2024OpenSearch, 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 р.

Дослідження з Claude. Пошук і звірку першоджерел зроблено з Claude (Anthropic). Відбір, текст і перевірку фактів — редакція Anatomy of IT Product.

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

Поділіться з тим, хто працює над тією ж задачею.