L9. Одна задача в трьох моделях
Побачити, що одне й те саме питання до даних у трьох системах вимагає трьох різних моделей, і що нове питання посеред проєкту обходиться в кожній по-різному. Після роботи ви вмієте вибрати між обчисленням на льоту й збереженим підсумком, між вкладенням і посиланням у документах, знаєте, чому таблиця Scylla відповідає на один запит, і можете показати це на числах.
Завдання
Section titled “Завдання”Предметна область: відгуки й рейтинг товарів продавця. Вона підходить, бо рейтинг це агрегат. 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тощо, таблиця з keyspacel09.
Окрім запитів, ви пишете й завантажувачі: схема й дані для 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 чи запитами без індексу: чекер їх відхилить, і саме на цьому лабораторна вчить.
Перед початком
Section titled “Перед початком”- Прочитайте модулі 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перевіряє одну систему.
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. Чекер їх враховує.
-
Заготовки. Скопіюйте заготовки запитів і шаблон звіту:
Terminal window cd labs/l09-three-modelscp -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. -
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(Датасет) планувальник інколи обирає послідовне читання й правильно робить), і виконайте його. -
Наївно: модель «як у 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). Потрібен індекс, який збігається з умовою запитуЗапам’ятайте обидва повідомлення: далі ви прибираєте їхні причини.
-
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. -
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. -
Нова вимога, посеред роботи. Коли всі три системи проходять
./check.shбез--new, прийшла нова вимога від команди кабінету покупця:Файл Питання Параметри Поля результату Порядок q6мої відгуки customer_id,limitreview_id,product_id,product_title,rating,title,helpful_votescreated_atвід нового до старого, за однакового часу більшийreview_idпершим; відгуки за всіма товарамиproduct_titleце назва товару. Додайтеq6у всі три моделі й подивіться, що кожна просить: індекс, поле, таблицю, перезавантаження. Нічого, крім потрібного, не переписуйте: чим менше довелося змінити, тим краща початкова модель. Запишіть уsolution/report.mdдля кожної системи: що довелося змінити й чому. -
Здача.
./check.sh --newперевіряє всі шість шаблонів і звіт.
Перевірка
Section titled “Перевірка”./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), але цього чекер не міряє.
Часті помилки
Section titled “Часті помилки”Рейтинг, округлений $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).
Далі, якщо цікаво
Section titled “Далі, якщо цікаво”Доведіть модель Scylla до продакшену: додайте відгук у reviews_by_product і перерахуйте підсумок у product_card одним пакетом (BATCH), подивіться на втрату атомарності між партиціями й подумайте, яке значення консистентності обрати. Повторіть q2 у DynamoDB за моделлю з модуля 15 і порівняйте число одиниць читання з трьома системами цієї роботи. Додайте четверту вимогу: «найкорисніші відгуки продавця» (за helpful_votes за всіма його товарами), і порівняйте, у якій системі це дешевше.