Перейти до вмісту

L9. Одна задача в трьох моделях

просунутийспирається на модуль 15, модуль 16, модуль 17

Побачити, що одне й те саме питання до даних у трьох системах вимагає трьох різних моделей, і що нове питання посеред проєкту обходиться в кожній по-різному. Після роботи ви вмієте вибрати між обчисленням на льоту й збереженим підсумком, між вкладенням і посиланням у документах, знаєте, чому таблиця Scylla відповідає на один запит, і можете показати це на числах.

Предметна область: відгуки й рейтинг товарів продавця. Вона підходить, бо рейтинг це агрегат. PostgreSQL рахує його запитом, MongoDB зберігає підсумок у документі товару, а Scylla не вміє ні JOIN, ні довільного GROUP BY і тримає готову відповідь у таблиці під кожен запит.

Етап 1 має п’ять патернів доступу. У кожному limit дорівнює 5 або 10.

Файл Питання Параметри Поля результату Порядок
q1 картка товару з рейтингом product_id product_id, title, price, seller_id, reviews_count, avg_rating один рядок або жодного
q2 останні відгуки товару product_id, limit review_id, customer_id, rating, title, helpful_votes created_at від нового до старого, за однакового часу більший review_id першим
q3 найкращі товари продавця seller_id, limit product_id, title, reviews_count, avg_rating лише товари з трьома відгуками й більше; avg_rating за спаданням, reviews_count за спаданням, product_id за зростанням
q4 рейтинг продавця seller_id seller_id, name, reviews_count, avg_rating відгуки всіх товарів продавця разом
q5 скарги на товар product_id, limit ті самі поля, що в q2 відгуки з оцінкою 1 або 2, порядок як у q2

avg_rating це середня оцінка, округлена до двох знаків half-up (як round(numeric, 2) у PostgreSQL), null, якщо відгуків немає; reviews_count у такому разі 0. Сортування q3 іде за округленим значенням. Неіснуючий товар чи продавець дає порожній результат, а існуючий продавець без відгуків дає один рядок із нулем.

Результат роботи, три каталоги в labs/l09-three-models/solution/:

  • pg/q1.sql … q5.sql: один запит SELECT на файл із параметрами :product_id, :seller_id, :limit.
  • mongo/q1.js … q5.js: у кожному файлі одна стрілкова функція від об’єкта параметрів, що повертає курсор: (p) => db.reviews.find({ product_id: p.product_id }).sort(…).limit(p.limit). Годиться і db.колекція.aggregate([…]). Якщо в документі немає першого поля результату, чекер бере _id.
  • scylla/q1.cql … q5.cql: один запит SELECT на файл, параметри :product_id тощо, таблиця з keyspace l09.

Окрім запитів, ви пишете й завантажувачі: схема й дані для MongoDB (база l09) і Scylla (keyspace l09), індекси в PostgreSQL. Це частина завдання: модель визначає, як вантажити. Чекер їх не запускає, а перевіряє те, що лежить у базах.

Готово, коли ./labs/l09-three-models/check.sh виводить «усе гаразд», а після нової вимоги з етапу 6 те саме робить ./labs/l09-three-models/check.sh --new.

Чого робити не треба. Правити check.sh, lib/ і expected/. Змінювати таблиці датасету в PostgreSQL (індекси додавати можна й треба). Користуватись ALLOW FILTERING чи запитами без індексу: чекер їх відхилить, і саме на цьому лабораторна вчить.

  • Прочитайте модулі 15 (моделювання від запитів), 16 (вкладення чи посилання, індекси, explain) і 17 (партиція, кластеризація, таблиця на запит). Модуль 5 знадобиться для денормалізації.
  • Потрібні лише Docker і bash. Чекер сам запускає Node.js у контейнері node:24.14.1-alpine; його залежності ставляться один раз у Docker-том l09_checker_node_modules (після роботи: docker volume rm l09_checker_node_modules).
  • Усі три бази важкі: PostgreSQL, MongoDB і Scylla разом займають до 4.5 ГБ пам’яті. Поки працюєте з однією, піднімайте лише її профіль, а check.sh --only pg|mongo|scylla перевіряє одну систему.
Terminal window
docker compose --profile postgres --profile mongo --profile scylla up -d --wait
./setup/load-dataset.sh small # якщо в PostgreSQL ще немає датасету (див. setup/README.md)

Назву проєкту й порти можна задати змінними: COMPOSE_PROJECT_NAME, PG_PORT, MONGO_PORT, SCYLLA_PORT. Чекер їх враховує.

  1. Заготовки. Скопіюйте заготовки запитів і шаблон звіту:

    Terminal window
    cd labs/l09-three-models
    cp -r starter/pg starter/mongo starter/scylla starter/report.md solution/

    У кожному файлі коментар повторює контракт: параметри, поля, порядок. starter/lib.sh містить допоміжні функції завантажувачів: pg_run (SQL у PostgreSQL), mongo_import (JSON-документи в колекцію), cql_run, scylla_copy.

  2. PostgreSQL: нормалізовано й з індексами. Таблиці products, reviews, sellers уже є, тож модель це запити й індекси. Напишіть solution/pg/q1.sql … q5.sql, потім ./check.sh --only pg. Дві пастки в даних: товар і продавець без відгуків мають лишитись у результаті з нулем (LEFT JOIN, а не JOIN), а відгуки з однаковим created_at треба впорядкувати за review_id. Створіть в solution/pg/model.sql індекси, під які запити мають іти (EXPLAIN покаже, чи вони використовуються: на small (Датасет) планувальник інколи обирає послідовне читання й правильно робить), і виконайте його.

  3. Наївно: модель «як у PostgreSQL». Завантажте таблиці в Scylla й колекції в MongoDB один до одного й подивіться, що скаже чекер:

    Terminal window
    ./starter/relational-copy/scylla/load.sh
    ./starter/relational-copy/mongo/load.sh

    Для Scylla в starter/relational-copy/scylla/ лежать запити, написані так, як звикли в SQL. Скопіюйте їх у solution/scylla/ і запустіть ./check.sh --only scylla:

    НІ Scylla, q2: останні відгуки товару: запит читає одну партицію, без ALLOW FILTERING
    запит містить ALLOW FILTERING: він дозволяє прочитати все й відкинути зайве, тобто це повне сканування. Потрібна таблиця, ключ партиції якої збігається з умовою запиту

    Для MongoDB напишіть q2.js над колекцією reviews (find за product_id, сортування, limit). Результат правильний, а план ні:

    НІ MongoDB, q2: останні відгуки товару: запит іде індексом (explain без COLLSCAN і без сортування в пам'яті)
    у плані COLLSCAN: запит читає всю колекцію (параметри: product_id=2410, limit=5). Потрібен індекс, який збігається з умовою запиту

    Запам’ятайте обидва повідомлення: далі ви прибираєте їхні причини.

  4. MongoDB: вкладення чи посилання. Вирішіть, що лежатиме в документі товару, що окремо і які індекси потрібні. Питання для рішення: де зберігати reviews_count і avg_rating, щоб q1 і q3 не рахували їх на льоту (q3 сортує за ними, а сортувати можна лише за збереженим, і чекер не пропустить сортування в пам’яті); чи вкладати відгуки в товар; який індекс дає порядок q2 і q5 без стадії SORT. Завантажувач ви пишете самі: запит у PostgreSQL видає по одному JSON-документу в рядку, mongoimport кладе їх у колекцію (зразок у starter/relational-copy/mongo/load.sh). Числа з десятковою частиною для price передайте як {"$numberDecimal": "499.00"}, час як {"$date": "2025-12-31T23:59:00Z"}. Напишіть solution/mongo/load.sh і q1.js … q5.js, перевіряйте ./check.sh --only mongo.

  5. Scylla: таблиця на запит. Для кожного запиту знайдіть ключ партиції, що збігається з його умовою, і ключі кластеризації, що дають потрібний порядок. Таблиці не з’єднуються, тож рейтинг, підсумки й копії назв (title) лежать у самих таблицях і рахуються під час завантаження. Дані беріть із PostgreSQL: COPY (SELECT …) TO STDOUT WITH (FORMAT csv) у cqlsh -e "COPY l09.таблиця (колонки) FROM STDIN", зразок у starter/relational-copy/scylla/load.sh. Keyspace створюйте з tablets = {'enabled': false} (як у завантажувачі модуля 17). Напишіть solution/scylla/load.sh і q1.cql … q5.cql, перевіряйте ./check.sh --only scylla.

  6. Нова вимога, посеред роботи. Коли всі три системи проходять ./check.sh без --new, прийшла нова вимога від команди кабінету покупця:

    Файл Питання Параметри Поля результату Порядок
    q6 мої відгуки customer_id, limit review_id, product_id, product_title, rating, title, helpful_votes created_at від нового до старого, за однакового часу більший review_id першим; відгуки за всіма товарами

    product_title це назва товару. Додайте q6 у всі три моделі й подивіться, що кожна просить: індекс, поле, таблицю, перезавантаження. Нічого, крім потрібного, не переписуйте: чим менше довелося змінити, тим краща початкова модель. Запишіть у solution/report.md для кожної системи: що довелося змінити й чому.

  7. Здача. ./check.sh --new перевіряє всі шість шаблонів і звіт.

Terminal window
./labs/l09-three-models/check.sh # q1–q5 у трьох системах
./labs/l09-three-models/check.sh --only mongo # одна система
./labs/l09-three-models/check.sh --new # плюс нова вимога q6 і звіт
./labs/l09-three-models/check.sh --new інший/каталог

Чекер виконує запити з вашого каталогу в живих базах для кількох наборів параметрів, серед яких є крайові: товар без відгуків, неіснуючий продавець, відгуки з однаковим часом, оцінки, що округлюються рівно посередині (3.875 має дати 3.88). Результат порівнюється з еталоном із PostgreSQL: рядки, поля й порядок. Окремо:

  • PostgreSQL: лише результат. Чи використовується індекс, чекер не оцінює: на датасеті small планувальник правильно читає таблицю послідовно, і вимога «іти індексом» тут була б хибною.
  • MongoDB: explain("executionStats") кожного запиту без COLLSCAN і без сортування в пам’яті (стадія SORT, а також $sort після $unwind чи $lookup, коли сортуються прочитані документи, а не згорнуті групи).
  • Scylla: аналіз тексту CQL, а не трасування. Запит не має містити ALLOW FILTERING, а умова мусить задавати рівністю чи IN усі колонки ключа партиції таблиці (чекер бере їх із system_schema). Це те саме, що кажуть про повне сканування у модулі 17: без ключа партиції запит торкається всіх. Аналіз тексту обрано замість трасування, бо він детермінований, називає причину і не залежить від обсягу даних: на small повне сканування займає мілісекунди й нічим не виказує себе.
  • Звіт: report.md існує, TODO прибрано, для кожної з трьох систем є рядок таблиці зі змістовним текстом.

Чекер не змінює ваші бази. Якщо його допоміжний процес (контейнер із Node, з’єднання з базою) падає, він друкує «збій чекера» й завершується з помилкою, а не мовчки пропускає перевірку.

Чого чекер не бачить: розміру документів і зростання масивів. Модель, де відгуки лежать масивом у документі товару, проходить етап 1, і лише нова вимога покаже її межу: відгуки покупця розкидані по різних товарах, і порядок за часом із вкладених масивів дає тільки сортування в пам’яті, яке чекер відхиляє. Сама ж така модель небезпечна й без нової вимоги: документ товару росте разом з активністю покупців (модуль 16), але цього чекер не міряє.

Рейтинг, округлений $round. $round у MongoDB округлює рівно посередині до парного: для 4.125 дає 4.12, а еталон, як round(numeric, 2) у PostgreSQL, 4.13. Підсумок для завантаження найпростіше порахувати в PostgreSQL.

JOIN замість LEFT JOIN. Товар чи продавець без відгуків зникає з результату, а не повертається з нулем. На датасеті таких 2379 товарів із 3500 і 10 продавців зі 120.

Порядок без review_id. У «Крамниці» багато відгуків мають однаковий created_at (останню хвилину року), тож порядок лише за часом недетермінований. У Scylla review_id треба додати в ключі кластеризації, у MongoDB в індекс і в sort.

ALLOW FILTERING і вторинні індекси в Scylla. Перше чекер відхиляє за текстом. Друге (CREATE INDEX) його обходить, але запит за вторинним індексом не задає ключа партиції, і чекер скаже, якої колонки бракує. Індекс у Scylla не замінює таблицю на запит.

Сортування в пам’яті в MongoDB. Індекс { product_id: 1, created_at: -1 } не дає порядку за review_id при рівному часі: потрібен третій ключ { …, _id: -1 }. Те саме з порядком q3: усі три поля сортування мають бути в індексі після seller_id.

Рейтинг у ключі кластеризації. У q3 порядок задає avg_rating, тож він стоїть у ключах кластеризації, а значення ключа не можна оновити на місці: нова оцінка означає видалити рядок і вставити інший. Тут дані завантажуються один раз, а в застосунку, що пише відгуки, це окреме рішення.

Docker дивиться на віддалену машину. Якщо docker context ls показує зірочку біля SSH-адреси, docker compose працює там. docker context use desktop-linux (Docker Desktop) або default (Linux).

Доведіть модель Scylla до продакшену: додайте відгук у reviews_by_product і перерахуйте підсумок у product_card одним пакетом (BATCH), подивіться на втрату атомарності між партиціями й подумайте, яке значення консистентності обрати. Повторіть q2 у DynamoDB за моделлю з модуля 15 і порівняйте число одиниць читання з трьома системами цієї роботи. Додайте четверту вимогу: «найкорисніші відгуки продавця» (за helpful_votes за всіма його товарами), і порівняйте, у якій системі це дешевше.