vyvchy
    Теми розділу

    05 · Бази даних і SQL для QA

    NoSQL для QA: MongoDB, Redis і консистентність

    Зміст

    Реляційну базу QA бачить наскрізь: таблиці, зовнішні ключі, JOIN, транзакція або пройшла, або відкотилася. Але щойно застосунок додає MongoDB для профілів користувачів, Redis для кешу й починає читати дані з репліки — старі інтуїції ламаються. Запис пройшов, а тест його не бачить. Документ у колекції має поле, якого немає в сусіднього документа. Кнопка показує стару ціну, хоча бекенд її вже оновив. Це не баги застосунку — це поведінка NoSQL-сховищ, яку треба розуміти, щоб не заводити фальшиві дефекти й не писати флакі-тести (flaky tests).

    На співбесіді senior-QA питають NoSQL не для того, щоб ви написали агрегацію в MongoDB напам'ять. Питають, щоб перевірити, чи розумієте ви компроміси: чому гнучка схема економить час розробнику й породжує клас багів для тестувальника, чому кеш пришвидшує систему й водночас бреше, чому «дані ж записалися» не дорівнює «дані видно всюди». Це розмова про консистентність, а не про синтаксис.

    SQL vs NoSQL: що насправді відрізняється

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

    Головна відмінність — де живе схема. Реляційна БД використовує schema-on-write: структуру визначено наперед у CREATE TABLE, і сервер відхилить рядок, що їй не відповідає (див. DDL і обмеження цілісності). Багато NoSQL-сховищ працюють за schema-on-read: сховище прийме будь-що, а розбиратися зі структурою мусить код, який ці дані читає. Схема нікуди не зникла — вона просто переїхала з сервера в застосунок і в голову розробника. Для QA це означає, що гарантій цілісності, які раніше давала БД, тепер може не бути взагалі.

    АспектРеляційна (SQL)Типова NoSQL
    Модель данихТаблиці, рядки, зв'язкиДокументи / ключ-значення / граф / колонки
    СхемаЖорстка, на записГнучка, на читання
    Мова запитівСтандартний SQLУ кожної своя (API, JSON-фільтри, команди)
    Зв'язкиJOIN на боці БДДенормалізація або з'єднання в коді
    МасштабуванняІсторично вертикальнеЗдебільшого горизонтальне (шардинг)
    ГарантіїЗдебільшого ACIDЧасто BASE / eventual consistency

    ACID (див. Транзакції та ACID) — це строгість: транзакція атомарна й лишає БД у консистентному стані. Багато розподілених NoSQL-сховищ натомість сповідують BASEBasically Available, Soft state, Eventual consistency: система завжди відповідає, стан тимчасово може бути неузгодженим, але «зрештою» вирівняється.

    Важлива засторога проти застарілих кліше: межа розмилася. PostgreSQL вміє зберігати й індексувати JSONB, а MongoDB від версії 4.0 підтримує багатодокументні ACID-транзакції. Тож «SQL = надійно, NoSQL = швидко» — не аргумент рівня senior. Правильна відповідь завжди «залежить від моделі доступу й налаштувань».

    Типи NoSQL оглядово

    «NoSQL» об'єднує щонайменше чотири різні моделі, і плутати їх — типова помилка на співбесіді. Redis і MongoDB — це не «дві схожі NoSQL-бази», а зовсім різні інструменти для різних задач.

    ТипМодельПрикладиТипове застосування
    ДокументнаJSON-подібні документиMongoDB, CouchDBПрофілі, каталоги, гнучкі сутності
    Ключ-значенняkeyvalueRedis, Memcached, DynamoDBКеш, сесії, лічильники, черги
    ШирококолонковаРядки з динамічними колонкамиCassandra, HBaseЛоги, часові ряди, величезні обсяги
    ГрафоваВузли й ребраNeo4jСоцзв'язки, рекомендації, маршрути

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

    MongoDB: документи, find і гнучка схема як клас багів

    MongoDB зберігає дані не рядками в таблицях, а документами в колекціях (collections). Документ — це структура на кшталт JSON (насправді бінарний формат BSON), у якої є вкладеність і масиви. У кожного документа є поле _id — унікальний ідентифікатор (за замовчуванням тип ObjectId).

    Запити пишуться не мовою SQL, а через API з фільтром-документом. Аналог SELECT * FROM users WHERE status = 'active' виглядає так:

    db.users.find({ status: "active" })

    Фільтр — це документ, де ключ це поле, а значення — умова. Оператори теж документи: db.orders.find({ total: { $gt: 100 } }) поверне замовлення з сумою понад 100. Уже тут криється перша пастка QA: якщо в базі total записано рядком "100", а фільтр порівнює з числом 100, документ не знайдеться — MongoDB не робить неявного приведення типів між рядком і числом. Тест «нічого не повертає», а даних насправді повно.

    Але головне для тестувальника — гнучка схема як окремий клас багів. У межах однієї колекції документи можуть мати різний набір полів. Розробник додав поле phone до нових користувачів — але тисячі старих документів його не мають. Формально це не помилка бази: MongoDB приймає обидва варіанти. Наслідки:

    • Код, який очікує user.phone, отримує undefined на старих документах і падає або мовчки поводиться інакше.
    • Поле перейменували (isActivestatus), нові документи пишуть по-новому, старі лишилися по-старому, а міграції не було. Половина даних читається неправильно.
    • null у полі й відсутнє поле — різні стани, які легко сплутати: фільтр рівності { phone: null } контрінтуїтивно знаходить обидва (і явний null, і документи без поля), а розрізняють їх лише оператори $exists та $type. Тест, якому потрібен один конкретний стан, має фільтрувати явно.

    MongoDB уміє валідувати схему ($jsonSchema на рівні колекції), але це опційно й часто не ввімкнено. Тому перевірка узгодженості документів — реальна робота QA, а не гарантія бази. Це той самий ризик денормалізації й розсинхрону, що й у проєктуванні схеми, тільки без сітки безпеки з боку сервера.

    Коли assert у базі доречний (наприклад, треба переконатися, що дія в UI справді створила документ), підключення з автотесту виглядає звично:

    import { MongoClient } from "mongodb";
    
    const client = await MongoClient.connect(process.env.MONGO_URI!);
    const users = client.db("shop").collection("users");
    
    const doc = await users.findOne({ email: "qa@example.com" });
    expect(doc?.status).toBe("active");

    Дисципліна read-only на спільних стендах діє так само, як для реляційних БД (див. БД і QA): перевіряємо стан, не псуємо чужі дані.

    Redis як кеш: TTL, інвалідація і «протухлий кеш»

    Redis — це сховище ключ-значення, яке тримає дані в оперативній пам'яті (може й скидати на диск, але працює насамперед у RAM). Звідси його швидкість і головне застосування — кеш перед повільнішою основною БД, а також сесії, лічильники й черги. Команди виконуються фактично в одному потоці, тож окрема операція атомарна.

    Класичний патерн — cache-aside («кеш збоку»): застосунок спершу питає Redis, і лише за промаху йде в основну БД, а результат кладе в кеш на майбутнє.

    Так, HIT

    Ні, MISS

    якщо крок пропущено

    Запит даних

    Є в Redis?

    Віддати з кешу

    Читання з основної БД

    Покласти в Redis з TTL

    Оновлення даних у БД

    Інвалідувати або оновити ключ

    Кеш віддає стару копію

    Так, HIT

    Ні, MISS

    якщо крок пропущено

    Запит даних

    Є в Redis?

    Віддати з кешу

    Читання з основної БД

    Покласти в Redis з TTL

    Оновлення даних у БД

    Інвалідувати або оновити ключ

    Кеш віддає стару копію

    Щоб кеш не тримав дані вічно, ключам ставлять TTL (time to live) — час життя в секундах. Команда SET price:42 99 EX 60 кладе значення на 60 секунд; TTL price:42 показує, скільки лишилося; коли час минув, ключ зникає сам. TTL — це найпростіша, «пасивна» інвалідація: дані оновляться не миттєво, а протягом вікна TTL.

    Звідси головний біль — інвалідація кешу й «протухлий» (stale) кеш. Дані в основній БД змінилися, а в Redis лежить стара копія, бо:

    • TTL великий, а активної інвалідації немає — стара ціна висить хвилини;
    • код оновив БД, але забув оновити чи видалити ключ (крок з пунктиру на діаграмі пропущено);
    • інвалідація прилетіла, але між «прочитали з БД» і «записали в кеш» встиг втрутитися новий запис (гонка, race condition).

    Для QA це означає конкретну тактику: якщо тест бачить старе значення, перша гіпотеза — не баг логіки, а кеш. Треба знати, який TTL у ключа, чи є примусова інвалідація, і чи не читає ваш тест кешовану відповідь замість свіжої. Це серверний аналог того самого «тест бачить стару версію», що й у HTTP-кешуванні браузера, тільки шар інший: там кеш між браузером і мережею, тут — між застосунком і базою. Плутати їх не можна: чистий browser context у Playwright прибирає браузерний кеш, але на Redis усередині бекенда не впливає ніяк.

    Eventual consistency: чому запис є, а його не видно

    Розподілені сховища зберігають кілька копій даних (реплік) заради надійності й масштабу. Запис іде на головний вузол (primary), а на репліки розповзається асинхронно. Проміжок між «записано на primary» і «видно на репліці» називають лагом реплікації (replication lag). Якщо застосунок читає з репліки одразу після запису, він може отримати старі дані — і це не збій, а штатна поведінка, яку називають eventual consistency: копії узгодяться «зрештою», а не миттєво.

    РеплікаPrimaryЗастосунокРеплікаPrimaryЗастосунокРепліка наздоганяє primaryЗапис: ціна = 99OK, записаноЧитання ціни89 (стара, реплікація в дорозі)Читання ціни99 (узгоджено)РеплікаPrimaryЗастосунокРеплікаPrimaryЗастосунокРепліка наздоганяє primaryЗапис: ціна = 99OK, записаноЧитання ціни89 (стара, реплікація в дорозі)Читання ціни99 (узгоджено)

    У MongoDB це керовано. Рівень підтвердження запису (write concern) визначає, скільки вузлів мають підтвердити запис; починаючи з версії 5.0, за замовчуванням це підтвердження більшістю вузлів. Куди читати, задає read preference: за замовчуванням читання йде з primary й тоді узгоджене, але якщо явно спрямувати читання на secondary заради розвантаження — можна впіймати застарілі дані. Тобто «баг неузгодженості» часто насправді закладено в конфігурацію.

    Практичний висновок для автотестів прямий: не покладайтеся на миттєву видимість запису й не лікуйте це sleep-ом. Правильний підхід — полінг (polling) з таймаутом: перевіряти умову з інтервалом, поки вона не стане істинною або не збіжить час. Фіксована затримка або зробить тест повільним, або лишить його флакі, бо лаг непостійний. Цю ж логіку «дочекайся консистентності, а не спи» ви зустрінете і в автотестах з БД, і в API-тестуванні, де полінг замінює sleep за тією самою причиною.

    CAP-теорема на пальцях

    CAP-теорема (сформульована Еріком Брюером, доведена Гілбертом і Лінчем 2002 року) описує розподілену систему трьома властивостями:

    • C — Consistency (узгодженість): кожне читання бачить останній запис або помилку.
    • A — Availability (доступність): кожен запит отримує відповідь (не помилку), хай навіть без гарантії свіжості.
    • P — Partition tolerance (стійкість до розділення): система працює, навіть коли мережа між вузлами розірвалася.

    Суть, яку часто переказують неправильно: це не «оберіть будь-які два з трьох назавжди». У реальній розподіленій системі мережеві розділення (partitions) неминучі, тож P — не опція, від якої можна відмовитися. Справжній вибір настає під час партиції: або зберегти узгодженість і відмовити частині запитів (CP), або лишитися доступним ціною можливо застарілих даних (AP). Коли ж мережа справна, система віддає і C, і A — компроміс включається лише в момент збою.

    Класифікувати конкретну СУБД однією парою літер — спрощення: поведінка залежить від конфігурації (рівень підтвердження запису, звідки читаємо). Але як орієнтир: MongoDB із читанням з primary тяжіє до CP — під час партиції радше відмовить у записі, ніж віддасть неузгоджене; Cassandra з типовими налаштуваннями тяжіє до AP — відповість завжди, а розбіжності владнає згодом. Ширше цю ідею розвиває PACELC: якщо є партиція (P), обираємо між A і C; інакше (Else) — між latency і consistency, бо навіть без збою сильніша узгодженість коштує затримки.

    Для QA CAP — не абстракція, а джерело сценаріїв: що станеться з формою під час короткого мережевого збою? Система поверне 5xx (CP — обрала узгодженість) чи прийме запит, який потім розійдеться з іншою реплікою (AP)? Розуміння того, який компроміс обрано, підказує, який «баг» під час деградації насправді є задокументованою поведінкою.

    Типові помилки

    • Виглядає як «загублене поле в базі», а насправді гнучка схема без міграції. Тест падає на undefined, бо старі документи не мають нового поля. Баг не в тесті й не в базі — бракує міграції даних.
    • Виглядає як «find нічого не повертає», а насправді не збігся тип. Фільтр шукає число, а в документі рядок (або ObjectId порівнюють із рядком). MongoDB не приводить типи мовчки — збіг не спрацьовує.
    • Виглядає як баг ціни на UI, а насправді протухлий кеш Redis. Бекенд оновив дані, але ключ не інвалідовано або TTL завеликий. Лікується не в UI, а перевіркою інвалідації кешу.
    • Виглядає як «запис пропав», а насправді eventual consistency. Записали на primary, прочитали з репліки до того, як доїхала реплікація. Ловиться полінгом, а не sleep і не баг-репортом.
    • Виглядає як падіння сервісу на секунду, а насправді CP-система під час партиції. Система свідомо відмовила в записі, щоб не порушити узгодженість. Це компроміс за дизайном, а не дефект.
    • Виглядає як «NoSQL швидше за SQL», а насправді залежить від патерну доступу. Redis-кеш і повне сканування колекції MongoDB — різні всесвіти швидкодії. Абсолютних тверджень тут не буває.

    Підсумок

    • NoSQL — не «без схеми», а schema-on-read: перевірка цілісності переїхала з БД у застосунок і стала роботою QA.
    • MongoDB зберігає гнучкі документи; різнорідність полів у колекції й неявне порівняння типів — окремий клас багів, а не помилка бази.
    • Redis — кеш у пам'яті з TTL; головний ризик QA — протухлі дані через слабку інвалідацію, і це серверний, а не браузерний рівень кешу.
    • Eventual consistency означає «зрештою», а не «миттєво»: після запису читання з репліки може бути старим — тестуй полінгом, не sleep.
    • CAP — це вибір C проти A під час мережевої партиції, а не «два з трьох назавжди»; знання обраного компромісу пояснює поведінку системи під час збоїв.

    Що питають на співбесіді

    • «У чому різниця між SQL і NoSQL?» Слабка відповідь — «NoSQL без схеми і швидший». Сильна — про schema-on-write проти schema-on-read, ACID проти BASE, і обов'язково «залежить від моделі й задачі», з прикладом, що PostgreSQL має JSONB, а MongoDB — транзакції.
    • «Які типи NoSQL знаєте?» Інтерв'юер перевіряє, чи не зливаєте ви Redis і MongoDB в одне. Назвіть чотири моделі (документна, ключ-значення, ширококолонкова, графова) з прикладом і застосуванням кожної.
    • «Чим гнучка схема небезпечна для тестування?» Тут чекають клас багів: різні поля в документах однієї колекції, null проти відсутнього поля, розсинхрон після перейменування без міграції.
    • «Що таке eventual consistency і як її тестувати?» Дивляться, чи не запропонуєте ви sleep. Правильно — пояснити лаг реплікації й полінг з таймаутом.
    • «Розкажіть про CAP.» Провал — «оберіть два з трьох». Сильна відповідь — що P неминуча, а вибір між C і A виникає лише під час партиції; бонус — PACELC і приклад CP проти AP.
    • «Тест бачить стару ціну — де шукати?» Перевіряють системність: HTTP-кеш браузера, кеш застосунку (Redis) з його TTL та інвалідацією, лаг реплікації — і вміння відрізнити ці рівні.

    Джерела

    • MongoDB Manual — офіційна документація: документи, find, валідація схеми, write/read concern, транзакції.
    • Redis Documentation — офіційна документація: структури даних, EXPIRE/TTL, патерни кешування й інвалідації.
    • Seth Gilbert, Nancy Lynch. «Brewer's Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services» (2002) — формальне доведення CAP-теореми.
    • Martin Kleppmann. «Designing Data-Intensive Applications» (O'Reilly, 2017) — канонічна книга про реплікацію, консистентність і компроміси розподілених сховищ.

    Примітка: силабус ISTQB CTFL 4.0 не має окремого розділу про NoSQL чи бази даних — це тема практичної роботи QA поза межами базового сертифіката, тож джерела вище канонічніші за силабус для цієї глави.