SQL для тестировщика: 20 запросов на реальной БД ShawarmaShop
SQL для Manual QA на реальной схеме ShawarmaShop: orders, payments, recipes, ingredients, JOIN, проверки API→DB, идемпотентность и переход к PostgreSQL/JDBC в Java автотестах.
SQL тестировщику нужен не ради строчки в резюме. Он нужен в тот момент, когда UI показал «заказ создан», API вернул 200, а тебе надо доказать, что backend действительно сохранил правильные данные — без дублей, неправильной суммы и сломанного статуса.
В этой статье не будет выдуманной учебной базы. Ниже используется реальная схема PostgreSQL учебного проекта ShawarmaShop от ThreadQA: recipes, ingredients, recipe_ingredients, orders, payments, restock_invoices, event_log и users.
- Открыть учебный стенд ShawarmaShop
Реальный интерфейс приложения для практики UI и поиска запросов в DevTools.
- Открыть Swagger UI ShawarmaShop
Контракт REST API, request/response schemas и endpoints.
Для QA безопасная база — read-only доступ. SELECT почти всегда достаточно, чтобы проверить persistence. UPDATE/DELETE в общей среде выполняй только по согласованному сценарию и никогда не используй production как песочницу.
Реальная схема ShawarmaShop: что с чем связано
Сначала не SQL, а модель данных. Если не понимаешь связи таблиц, даже синтаксически правильный JOIN легко даст ложный результат.
| Таблица | Что хранит | Ключевые поля для QA |
|---|---|---|
| recipes | Каталог шаурмы | id, name, size, price, prep_seconds, created_by, updated_by |
| ingredients | Склад | id, name, on_hand, min_level, supplier_channel |
| recipe_ingredients | Связь рецепт ↔ ингредиент | recipe_id, ingredient_id, qty_needed |
| orders | Заказы и их lifecycle | recipe_id, qty, total_price, status, placed_at, paid_at, completed_at |
| payments | Платежи | order_id, idempotency_key, amount, method, txn_id, status |
| restock_invoices | Инвойсы пополнения | ingredient_id, invoice_number, total_cost, status, eta |
| event_log | Read-модель Kafka events | type, payload jsonb, ts |
| users | Учётные записи | username, password_hash, created_at |
1recipes 1 ───── * orders
2recipes 1 ───── * recipe_ingredients * ───── 1 ingredients
3orders 1 ───── * payments
4ingredients 1 ─ * restock_invoices
5event_log: отдельная read-модель событий Kafka (payload JSONB)
6users: учётки; demo-модель single-userЖизненный цикл заказа
1PENDING → PAID → PREPARING → DONE
2 └──────────────→ CANCELLED (по разрешённому бизнес-сценарию)
3
4orders.paid_at = NULL до успешной оплаты
5orders.completed_at = NULL до DONE
6orders.bread_batch_id = NULL до брони лаваша через gRPC-пекарнюЭто уже готовый набор инвариантов для тестирования: у PENDING не должно быть completed_at, DONE должен иметь completed_at, а payment и order должны согласовываться между собой.
API → DB: как связать проверку
POST /api/v1/orders возвращает OrderResponse с id, status, recipe, qty, totalPrice, placedAt, etaAt и payment. Самый удобный correlation key — order id: его забираем из API и используем в SELECT.
11. POST /api/v1/orders
22. HTTP 200
33. response.id = 7812
44. response.status = PENDING
55. SELECT ... FROM orders WHERE id = 7812
66. сравнить recipe_id / qty / total_price / status20 SQL-запросов, которые реально пригодятся QA
1. Найти заказ по ID из API response
1SELECT *
2FROM orders
3WHERE id = 7812;Это базовая сквозная проверка после POST /api/v1/orders или GET /api/v1/orders/{id}.
2. Выбрать только поля, которые сравниваем с API
1SELECT id, recipe_id, qty, total_price, status, placed_at, eta_at
2FROM orders
3WHERE id = 7812;После создания заказа ожидаем PENDING. qty и total_price должны согласовываться с OrderResponse.qty и OrderResponse.totalPrice.
3. Проверить lifecycle timestamps заказа
1SELECT id, status, placed_at, paid_at, completed_at, bread_batch_id
2FROM orders
3WHERE id = 7812;Для PENDING логично ожидать paid_at/completed_at = NULL. После успешной оплаты paid_at должен появиться; после DONE — completed_at. Конкретные переходы проверяй против требований проекта.
4. Последние заказы
1SELECT id, recipe_id, qty, total_price, status, placed_at
2FROM orders
3ORDER BY placed_at DESC
4LIMIT 10;На общем стенде не используй «самый последний заказ» как единственный способ найти свои данные: другой пользователь или параллельный тест может создать запись позже.
5. Найти невозможный status
1SELECT id, status
2FROM orders
3WHERE status NOT IN ('PENDING', 'PAID', 'PREPARING', 'DONE', 'CANCELLED');Даже если БД или приложение ограничивают enum на уровне кода, такой запрос полезен при миграциях и импортах данных.
6. Найти подозрительные lifecycle-состояния
1SELECT id, status, paid_at, completed_at
2FROM orders
3WHERE (status = 'PENDING' AND paid_at IS NOT NULL)
4 OR (status = 'DONE' AND completed_at IS NULL);Это пример проверки бизнес-инвариантов. Перед тем как заводить баг, обязательно сверяй найденное состояние с реальными требованиями и асинхронностью процесса.
7. JOIN заказа и рецепта
1SELECT
2 o.id AS order_id,
3 o.status,
4 o.qty,
5 o.total_price,
6 r.id AS recipe_id,
7 r.name AS recipe_name,
8 r.size AS recipe_size,
9 r.price AS current_recipe_price
10FROM orders o
11JOIN recipes r ON r.id = o.recipe_id
12WHERE o.id = 7812;Так можно сверить вложенный OrderResponse.recipe с фактическим recipes.id/name/size. Не сравнивай total_price с текущим recipes.price спустя долгое время без знания бизнес-правила: цена рецепта могла измениться после оформления заказа.
8. Найти рецепты по части имени и размеру
1SELECT id, name, size, price, prep_seconds
2FROM recipes
3WHERE name ILIKE '%сыр%'
4 AND size = 'LARGE'
5ORDER BY name;Это удобно сравнивать с GET /api/v1/recipes?query=...&recipeSize=LARGE.
9. Найти дубли имён рецептов
1SELECT name, COUNT(*)
2FROM recipes
3GROUP BY name
4HAVING COUNT(*) > 1;name объявлен UNIQUE NOT NULL, поэтому результат должен быть пустым. Хорошая проверка после миграций/импортов; PATCH /api/v1/recipes/{id} также документирует 409 при занятом имени.
10. Проверить состав рецепта через many-to-many
1SELECT
2 r.id AS recipe_id,
3 r.name AS recipe_name,
4 i.id AS ingredient_id,
5 i.name AS ingredient_name,
6 ri.qty_needed,
7 i.unit,
8 i.on_hand
9FROM recipe_ingredients ri
10JOIN recipes r ON r.id = ri.recipe_id
11JOIN ingredients i ON i.id = ri.ingredient_id
12WHERE r.id = 42
13ORDER BY i.name;Сравнивай с GET /api/v1/recipes/{id}/ingredients: API возвращает recipeName и список ingredientId/name/qtyNeeded/unit/onHand.
11. Найти LOW STOCK так же, как это делает бизнес-логика
1SELECT id, name, unit, on_hand, min_level, supplier_channel
2FROM ingredients
3WHERE on_hand < min_level
4ORDER BY (min_level - on_hand) DESC;Это естественная DB-проверка для GET /api/v1/ingredients?lowStock=true.
12. Проверить supplier channel
1SELECT id, name, supplier_code, supplier_channel, supplier_lead_time_days
2FROM ingredients
3WHERE supplier_channel IN ('SOAP', 'REST', 'GRPC')
4ORDER BY supplier_channel, name;Поле supplier_channel — хорошая точка для интеграционных сценариев: разные ингредиенты могут идти через SOAP, REST или gRPC.
13. История платежей по конкретному заказу
1SELECT id, order_id, idempotency_key, amount, method, status, txn_id, failure_reason, created_at
2FROM payments
3WHERE order_id = 7812
4ORDER BY created_at;Сверяй с GET /api/v1/orders/{id}/payments. Особенно полезно для повторных запросов оплаты.
14. Проверить идемпотентность платежа по Idempotency-Key
1SELECT idempotency_key, COUNT(*) AS payment_rows
2FROM payments
3WHERE idempotency_key = '550e8400-e29b-41d4-a716-446655440000'
4GROUP BY idempotency_key;После двух POST /api/v1/orders/{id}/pay с одним и тем же Idempotency-Key ожидаем одну логическую запись платежа. В БД ключ дополнительно UNIQUE NOT NULL — это отличный пример того, как API-требование поддерживается persistence-ограничением.
15. Сверить order и payment между собой
1SELECT
2 o.id AS order_id,
3 o.status AS order_status,
4 o.total_price,
5 o.payment_method AS order_payment_method,
6 p.amount AS payment_amount,
7 p.method AS payment_method,
8 p.status AS payment_status,
9 p.txn_id
10FROM orders o
11JOIN payments p ON p.order_id = o.id
12WHERE o.id = 7812
13ORDER BY p.created_at DESC;Проверяем согласованность amount/total_price и method/payment_method. Учитывай, что у заказа может быть история нескольких попыток платежа — не делай вывод по случайной строке без created_at/status.
16. Последние FAILED платежи
1SELECT id, order_id, amount, method, txn_id, failure_reason, created_at
2FROM payments
3WHERE status = 'FAILED'
4ORDER BY created_at DESC
5LIMIT 20;failure_reason должен помогать диагностировать отказ. Если причина теряется, это отдельный вопрос к observability и контракту.
17. Найти заказы без платежей
1SELECT o.id, o.status, o.total_price, o.placed_at
2FROM orders o
3LEFT JOIN payments p ON p.order_id = o.id
4WHERE p.id IS NULL
5ORDER BY o.placed_at DESC;Для нового PENDING-заказа это может быть нормальным до /pay. Главное правило SQL-тестирования: отсутствие строки не равно багу без бизнес-контекста.
18. Проверить инвойсы пополнения ингредиента
1SELECT
2 ri.id,
3 i.name AS ingredient_name,
4 ri.invoice_number,
5 ri.total_cost,
6 ri.status,
7 ri.eta,
8 ri.warehouse_code
9FROM restock_invoices ri
10JOIN ingredients i ON i.id = ri.ingredient_id
11WHERE ri.ingredient_id = 7
12ORDER BY ri.eta DESC;Это полезно после POST /api/v1/ingredients/{id}/restock, особенно для SOAP-поставщика и сценариев ACCEPTED / WAITLIST / REJECTED.
19. Посмотреть последние Kafka-события в event_log
1SELECT id, type, jsonb_pretty(payload) AS payload, ts
2FROM event_log
3ORDER BY ts DESC
4LIMIT 20;event_log — read-модель страницы EVENTS, которую наполняет Kafka-consumer. OpenAPI также отдаёт эти данные через GET /api/v1/events.
20. Отфильтровать конкретный тип события
1SELECT id, type, payload, ts
2FROM event_log
3WHERE type = 'ORDER_PLACED'
4ORDER BY ts DESC
5LIMIT 20;В схеме type хранит значения вроде ORDER_PLACED. Точную структуру payload не надо угадывать: сначала посмотри реальный JSON, затем уже пиши проверки JSONB по конкретным ключам.
Сильный тест: заказ → платёж → БД → Kafka
Теперь соберём всё в один сценарий, который отлично показывает разницу между «я умею SELECT» и «я умею тестировать backend».
11. POST /api/v1/orders
2 → HTTP 200
3 → сохранить orderId
4 → status = PENDING
5
62. SELECT FROM orders WHERE id = orderId
7 → recipe_id / qty / total_price / status совпадают с API
8
93. POST /api/v1/orders/{id}/pay
10 Header: Idempotency-Key = <UUID>
11
124. повторить тот же POST с тем же Idempotency-Key
13
145. SELECT FROM payments WHERE idempotency_key = <UUID>
15 → не появился второй платёж для того же ключа
16 → amount/method/status согласованы
17
186. SELECT FROM orders WHERE id = orderId
19 → проверить переход состояния и paid_at по фактическому результату оплаты
20
217. SELECT FROM event_log ORDER BY ts DESC
22 → найти связанные доменные события и проверить их payloadЕщё 5 вещей в схеме, которые должен заметить QA
- ▸recipes.name — UNIQUE NOT NULL: проверяем конфликт имён и поведение PATCH.
- ▸payments.idempotency_key — UNIQUE NOT NULL: persistence защищает от дубля при retry.
- ▸ingredients.on_hand и min_level задают LOW STOCK: это можно проверить и через API, и через SQL.
- ▸orders хранит paid_at, completed_at и bread_batch_id: lifecycle можно проверять не только по status.
- ▸event_log.payload — JSONB: сначала изучаем реальный payload, потом пишем JSONB-проверки, а не предполагаем его структуру.
А что с audit-полями?
В recipes, ingredients и orders есть created_at/updated_at/created_by/updated_by. Это полезно при тестировании PATCH и административных операций: можно проверить не только бизнес-поле, но и корректность аудита пользователя из JWT.
1SELECT id, name, created_at, updated_at, created_by, updated_by
2FROM recipes
3WHERE id = 42;Ошибки Manual QA при работе с SQL
Ошибка 1. Считать любую несостыковку багом
Асинхронность, очереди и background processing создают промежуточные состояния. Например, payment SUCCEEDED и переход заказа в PREPARING могут происходить не одной атомарной строкой кода. Учитывай SLA/требования и повторяй проверку осмысленно.
Ошибка 2. Искать свою запись через ORDER BY ... LIMIT 1
На shared environment используй id из API response, UUID/idempotency key или другой уникальный correlation key.
Ошибка 3. Забывать про snapshot-данные
orders.total_price — сумма конкретного заказа, а recipes.price — текущая цена рецепта. Если рецепт потом подорожал, их различие не обязано быть ошибкой.
Ошибка 4. Делать JOIN без понимания cardinality
orders → payments — one-to-many. Один заказ может иметь историю попыток оплаты, поэтому JOIN способен вернуть несколько строк. Всегда понимай, почему строк стало больше.
Ошибка 5. Трогать password_hash или секреты без необходимости
users.password_hash нужен системе, но почти никогда не нужен в обычной QA-выборке. Не выгружай чувствительные поля просто потому, что у тебя есть доступ.
Как эта же проверка выглядит в Java Automation
Manual QA сначала выполняет цепочку Postman → response → DBeaver → SELECT. Автотест может выполнить ту же проверку в одном запуске.
1long orderId = given()
2 .contentType(ContentType.JSON)
3 .body(orderRequest)
4.when()
5 .post("/api/v1/orders")
6.then()
7 .statusCode(200)
8 .body("status", equalTo("PENDING"))
9 .extract()
10 .jsonPath()
11 .getLong("id");
12
13OrderRow order = orderRepository.findById(orderId);
14
15assertThat(order.status()).isEqualTo("PENDING");
16assertThat(order.qty()).isEqualTo(orderRequest.qty());
17assertThat(order.recipeId()).isEqualTo(orderRequest.recipeId());Здесь OrderRow — уже не абстрактная сущность: он маппится на реальные orders.id / recipe_id / qty / total_price / status / placed_at и другие поля. Repository можно реализовать через JDBC или другой data-access слой.
А сценарий идемпотентности можно автоматизировать ещё сильнее: дважды вызвать /pay с одним Idempotency-Key, затем SQL-запросом проверить количество строк payments по этому ключу.
Именно поэтому переход из Manual QA в Java Automation логичен: ты не учишь новую профессию с нуля — ты переносишь уже знакомые проверки HTTP, API и БД в повторяемый код.
В Java QA Automation ThreadQA эти части соединяются вокруг ShawarmaShop: REST Assured для API, PostgreSQL/JDBC для persistence, Kafka для событий и Selenide для UI.
Практика: сделай эту проверку самостоятельно
- ▸1. Создай заказ через Swagger или Postman и сохрани orderId.
- ▸2. Найди запись в orders и сверь recipe_id, qty, total_price, status.
- ▸3. Через JOIN подтяни recipes и сравни вложенный recipe из API.
- ▸4. Оплати заказ с уникальным Idempotency-Key.
- ▸5. Повтори оплату с тем же ключом и проверь payments.
- ▸6. Посмотри order/payment lifecycle fields.
- ▸7. Найди связанные события в event_log и изучи реальный JSONB payload.
Если ты уверенно можешь пройти UI/API → DB → payment → event, то SQL уже стал рабочим инструментом тестировщика, а не набором SELECT для собеседования.
FAQ
Какой SQL нужен Manual QA?
Для ежедневной работы начни с SELECT, WHERE, IN, BETWEEN, NULL, ORDER BY, LIMIT, COUNT/SUM/AVG, GROUP BY/HAVING, INNER JOIN и LEFT JOIN. После этого добавляй CTE/subquery и JSONB под конкретные задачи проекта.
Нужно ли Manual QA знать структуру БД?
Не всю наизусть. Важно уметь быстро понять PK/FK, cardinality и поля бизнес-сущности, которую проверяешь. В ShawarmaShop для заказа это в первую очередь orders → recipes и orders → payments.
Что проверять в БД после API-запроса?
Не только факт существования строки. Проверяй бизнес-поля, lifecycle timestamps, связанные сущности, уникальность, отсутствие нежелательных дублей и согласованность данных между слоями.
Зачем тестировщику JSONB?
В ShawarmaShop event_log.payload хранится как jsonb. Для начала достаточно уметь посмотреть payload и понять структуру; затем можно фильтровать по конкретным JSON-ключам, когда известен реальный формат события.
Что важнее для перехода в Automation: SQL или Java?
SQL быстрее усиливает текущую manual-работу. Если цель — Automation, Java стоит учить параллельно: REST Assured создаёт данные через API, JDBC/PostgreSQL проверяет persistence, а дальше всё объединяется в один автотест.