UI-тест который кликает "Оформить заказ" и видит экран подтверждения не говорит тебе был ли заказ реально записан в базу данных. SQL говорит. Статья разбирает пять паттернов запросов которые покрывают 80% QA-работы с базами данных: SELECT с WHERE, JOIN, COUNT и GROUP BY, проверки NULL, и как запускать запросы напрямую из Playwright-тестов для верификации сохранения данных.

Почему QA-инженерам нужен SQL

UI-тесты верифицируют что видит пользователь. SQL верифицирует что реально произошло.

Когда тест кликает "Оформить заказ" и видит экран подтверждения, ты знаешь что UI ответил. Не знаешь сохранён ли заказ. Не знаешь правильный ли статус выставлен. Не знаешь правильно ли написан внешний ключ. Единственный способ верифицировать это: смотреть в базу данных напрямую.

Случаи использования SQL в QA:

  • Верифицировать что данные сохранены после UI-действия (отправка формы, загрузка файла, платёж)
  • Подготовить тестовые данные напрямую вместо 10 кликов через UI
  • Проверить данные которые UI не показывает (audit-логи, внутренние флаги, мягко удалённые записи)
  • Воспроизвести баги проверяя в каком состоянии была база когда что-то сломалось
  • Валидировать миграции после деплоя изменившего схему

Когда вакансия говорит "требуется опыт SQL": вот о чём идёт речь. Не хранимые процедуры, не оптимизация запросов, не администрирование базы данных. SELECT-запросы с условиями.

Инструмент: любой SQL-клиент

Знать конкретный продукт базы данных не нужно. Синтаксис запросов почти идентичен в PostgreSQL, MySQL и SQLite. Для практики:

  • TablePlus (Mac/Windows): чистый интерфейс, бесплатного уровня достаточно
  • DBeaver: бесплатный, работает с любым типом базы данных
  • psql: командная строка PostgreSQL, всегда доступна
  • Любой Playwright-тест с пакетом pg или mysql2 тоже может запускать запросы напрямую

Примеры ниже основаны на схеме базы данных lab.becomeqa.com.

Паттерн 1: SELECT

Самое частое что будешь делать:

-- Получить всех пользователей
SELECT * FROM users;

-- Получить конкретные столбцы
SELECT id, email, created_at FROM users;

-- Получить одного пользователя по email
SELECT * FROM users WHERE email = 'admin@becomeqa.com';

-- Получить пользователей созданных за последние 7 дней
SELECT * FROM users WHERE created_at > NOW() - INTERVAL '7 days';

SELECT * получает каждый столбец. SELECT id, email получает только указанные столбцы. WHERE фильтрует строки. Это 90% того что нужно.

В QA-контексте

-- После теста регистрации верифицировать что пользователь создан
SELECT id, email, role, is_active 
FROM users 
WHERE email = 'testuser_1234567@test.com';

Если возвращает строку: пользователь сохранён. Если ничего: регистрация тихо упала хотя UI показал успех.

Паттерн 2: WHERE с условиями

Комбинирование условий:

-- AND: оба условия должны быть истинны
SELECT * FROM items 
WHERE status = 'completed' AND user_id = 42;

-- OR: хотя бы одно условие должно быть истинно
SELECT * FROM items 
WHERE status = 'pending' OR status = 'in_progress';

-- IN: совпадение с любым значением в списке
SELECT * FROM items 
WHERE status IN ('pending', 'in_progress', 'completed');

-- NOT: исключить строки
SELECT * FROM items WHERE status != 'deleted';

-- LIKE: частичное совпадение строк (% - wildcard)
SELECT * FROM users WHERE email LIKE '%@test.com';

-- IS NULL: проверка на отсутствие значений
SELECT * FROM items WHERE deleted_at IS NULL;
SELECT * FROM items WHERE deleted_at IS NOT NULL;

В QA-контексте

-- Верифицировать что мягкое удаление сработало (deleted_at должен быть выставлен)
SELECT id, title, deleted_at 
FROM items 
WHERE id = 99;

-- Найти все тестовые данные для очистки после прогона
SELECT * FROM users WHERE email LIKE '%testuser_%@test.com';

Паттерн 3: JOIN

Реальные данные живут в нескольких таблицах. Travel-элемент принадлежит пользователю. Заказ принадлежит клиенту и содержит продукты. JOIN нужен чтобы видеть полную картину.

-- Базовый JOIN: элементы с email их владельца
SELECT items.id, items.title, items.status, users.email
FROM items
JOIN users ON items.user_id = users.id;

-- Фильтрация результата JOIN
SELECT items.id, items.title, users.email
FROM items
JOIN users ON items.user_id = users.id
WHERE users.email = 'admin@becomeqa.com';

Паттерн всегда такой:

SELECT [нужные столбцы]
FROM [основная таблица]
JOIN [связанная таблица] ON [как они соединяются]
WHERE [опциональный фильтр]

В QA-контексте

-- После добавления элемента как admin, верифицировать что он связан с правильным пользователем
SELECT items.title, items.status, users.email AS owner
FROM items
JOIN users ON items.user_id = users.id
WHERE items.title = 'Tokyo'
ORDER BY items.created_at DESC
LIMIT 1;

Паттерн 4: COUNT и агрегатные функции

Когда нужны числа, а не строки:

-- Сколько пользователей?
SELECT COUNT(*) FROM users;

-- Сколько активных пользователей?
SELECT COUNT(*) FROM users WHERE is_active = true;

-- Сколько элементов по каждому статусу?
SELECT status, COUNT(*) AS total
FROM items
GROUP BY status;

-- Самая поздняя дата создания элемента
SELECT MAX(created_at) FROM items;

-- Общее количество элементов по пользователю
SELECT user_id, COUNT(*) AS item_count
FROM items
GROUP BY user_id
ORDER BY item_count DESC;

В QA-контексте

-- После теста массового импорта верифицировать что создано правильное количество записей
SELECT COUNT(*) FROM items WHERE created_at > '2026-05-15 10:00:00';

-- Верифицировать изоляцию тестовых данных (нет перекрёстного заражения между прогонами)
SELECT user_id, COUNT(*) FROM items GROUP BY user_id;

Паттерн 5: ORDER BY и LIMIT

Управление какие строки получаешь и в каком порядке:

-- Самые недавно созданные элементы первыми
SELECT * FROM items ORDER BY created_at DESC;

-- Самые старые первыми
SELECT * FROM items ORDER BY created_at ASC;

-- Только 5 самых недавних
SELECT * FROM items ORDER BY created_at DESC LIMIT 5;

-- Страница 2 результатов (строки 11–20)
SELECT * FROM items ORDER BY id LIMIT 10 OFFSET 10;

В QA-контексте

-- После того как тест создал элемент, взять только что созданный
SELECT * FROM items 
WHERE user_id = 42 
ORDER BY created_at DESC 
LIMIT 1;

Полный верификационный запрос

Флоу теста: пользователь входит, добавляет travel-элемент с названием "Paris", помечает его как "Completed". Как верифицировать полную операцию в SQL:

SELECT 
    items.id,
    items.title,
    items.status,
    items.created_at,
    users.email AS owner
FROM items
JOIN users ON items.user_id = users.id
WHERE items.title = 'Paris'
    AND items.status = 'completed'
    AND users.email = 'admin@becomeqa.com'
ORDER BY items.created_at DESC
LIMIT 1;

Если возвращает строку: весь флоу отработал от начала до конца. Если ничего: что-то тихо упало между входом и обновлением статуса.

Использование SQL в Playwright-тестах

Можно запускать SQL напрямую из тест-кода используя клиентскую библиотеку базы данных:

import { test, expect } from '@playwright/test';
import { Client } from 'pg'; // npm install pg

test('item is saved to database after creation', async ({ page }) => {
    // UI-действие
    await page.goto('/');
    // ... вход, добавление элемента 'Tokyo' ...

    // Верификация в базе данных
    const db = new Client({ connectionString: process.env.DATABASE_URL });
    await db.connect();
    
    const result = await db.query(
        'SELECT * FROM items WHERE title = $1 ORDER BY created_at DESC LIMIT 1',
        ['Tokyo']
    );
    
    expect(result.rows.length).toBe(1);
    expect(result.rows[0].status).toBe('planned');
    
    await db.end();
});

Мощный паттерн: UI-действие плюс верификация в базе в одном тесте. UI-тест доказывает что приложение ответило корректно; запрос к базе данных доказывает что данные реально сохранились.

Частые ошибки

Использование SELECT в продакшн-тестах. Нормально для отладки, но в автотестах называй столбцы явно. Когда столбец добавляется или удаляется, SELECT скрывает изменение. Забыть WHERE при DELETE. Если очищаешь тестовые данные через DELETE FROM items WHERE email LIKE '%test%': всегда перепроверяй WHERE перед запуском. DELETE FROM items без WHERE удаляет всё. Не использовать параметризованные запросы в коде. Никогда не строй SQL-строки конкатенацией пользовательского ввода. Используй плейсхолдеры $1 как показано выше для защиты от SQL-инъекции. Читать устаревшие данные. В некоторых базах данных изоляция транзакций означает что нужно выполнить COMMIT транзакции прежде чем другое соединение увидит изменения. Если тест пишет данные и затем запрашивает их из другого соединения: убедись что транзакция завершена.

Что пока не нужно

  • Подзапросы
  • Оконные функции (ROW_NUMBER, RANK)
  • CTE (конструкции WITH)
  • Хранимые процедуры и функции
  • Индексы и планирование запросов
  • Проектирование схемы и нормализация

Всё это важно для разработчиков баз данных. Для QA задача: читать данные. Пять паттернов выше: вся работа в 95% случаев.

FAQ

На какой базе данных учить SQL?

PostgreSQL. Самый распространённый в современных веб-приложениях, лучший инструментарий, синтаксис достаточно стандартный чтобы переключиться на MySQL или SQLite за минуты. Установи TablePlus, подключись к любому PostgreSQL-инстансу, практикуйся там.

Можно ли практиковать SQL без реального приложения?

Да. Сайты sqlfiddle.com и db-fiddle.com позволяют создавать таблицы и запускать запросы прямо в браузере без установки. Создай таблицы users и items, вставь несколько строк, практикуй пять паттернов выше.

Писать SQL-тесты или использовать только для отладки?

Обе цели. SQL незаменим для ручной отладки. Когда не понимаешь почему тест падает: проверяй базу данных. И ценен в автотестах для верификации данных которую UI не может обеспечить. Начинай с отладки, добавляй автоматизированные database assertions когда освоишься с запросами.

В чём разница между SQL-базами данных?

Для QA-целей почти ни в чём. PostgreSQL использует $1 для параметров, MySQL использует ?, но синтаксис SELECT/WHERE/JOIN идентичен. Пять паттернов выше работают во всех трёх.

→ See also: Как работает интернет: объяснение для тестировщиков | API-тестирование в Playwright: выходим за рамки UI