Занимались ли вы оптимизацией и ускорением запросов к базе данных?

«Занимались ли вы оптимизацией и ускорением запросов к базе данных?» — вопрос из категории Базы данных, который задают на 25% собеседований C/C++ Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Да, оптимизация запросов была регулярной задачей, особенно в проектах с высокими требованиями к скорости отклика.

Основные применяемые техники:

  1. Анализ плана выполнения (EXPLAIN): Первым шатом всегда был анализ плана запроса в PostgreSQL (EXPLAIN ANALYZE) или SQLite (EXPLAIN QUERY PLAN), чтобы найти полные сканирования таблиц (seq scan), дорогие сортировки или неэффективные JOIN.
  2. Создание целевых индексов:
    • Для точечных запросов (WHERE id = ?) — обычный B-tree индекс.
    • Для префиксного поиска по строкам (WHERE name LIKE 'ABC%') — тоже B-tree.
    • Для запросов с диапазоном и сортировкой (WHERE date > ? ORDER BY date) — составной индекс с правильным порядком колонок.
      -- Пример индекса, покрывающего фильтрацию и сортировку
      CREATE INDEX idx_orders_user_status_created ON orders(user_id, status, created_at DESC);
  3. Оптимизация JOIN:
    • Убеждался, что поля для JOIN проиндексированы.
    • Старался уменьшать размер промежуточных результатов, применяя фильтры (WHERE) как можно раньше.
    • Для больших таблиц иногда использовал временные таблицы с индексами для сложных многоступенчатых агрегаций.
  4. Работа из C++ приложения:
    • Подготовленные выражения (Prepared Statements): Использовал их не только для безопасности от SQL-инъекций, но и для того, чтобы СУБД кэшировала план выполнения запроса.
      // Пример с SQLite3 C API
      sqlite3_stmt* stmt;
      const char* sql = "SELECT name FROM users WHERE email = ? AND active = 1;";
      sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr);
      sqlite3_bind_text(stmt, 1, userEmail.c_str(), -1, SQLITE_TRANSIENT);
      while (sqlite3_step(stmt) == SQLITE_ROW) {
      // Обработка результата
      }
      sqlite3_finalize(stmt);
    • Пакетная вставка: Для массовой загрузки данных использовал транзакции и многострочный INSERT (INSERT INTO ... VALUES (...), (...), ...) или специализированные утилиты вроде COPY в PostgreSQL, чтобы минимизировать сетевые издержки и накладные расходы на commit.
  5. Кэширование: Для данных, которые редко меняются, но часто читаются (например, справочники конфигурации), реализовывал кэш в памяти приложения (например, в std::unordered_map) с инвалидацией по TTL или при получении события об изменении из БД.