BI Consult Desktop Logo BI Consult Mobile Logo
  • Russian BI Исследование российских bi
  • Перейти на Fine BI
  • Контакты
  • +7 812 334-08-01
    +7 499 608-13-06
  • Отправить сообщение
  • Главная
  • Продукты Эксперт-BI
    • Дистрибуция
    • Розничная торговля
    • Производство
    • Операторы связи
    • Страхование
    • Банки
    • Лизинг
    • Логистика
    • Нефтегазовый сектор
    • Медицина
    • Сеть ресторанов
    • E-Commerce
    • Сельское хозяйство
    • Энергетика
    • FMCG
    • Девелоперы
    • Маркетплейсы
    • Пищевая промышленность
    • Фармацевтика
    • Построение Data Platform
    • Цифровая трансформация
    • Управление по KPI
    • Финансы
    • Продажи
    • Склад
    • HR
    • Маркетинг
    • Внутренний аудит
    • Категорийный менеджмент
    • S&OP и FP&A
    • Геоаналитика
    • Цепочки поставок (SCM)
    • AutoML
    • Process Mining
    • IBP
    • ИТ (CIO)
    • Закупки
  • Платформы
    • Системы бизнес-анализа (BI)
    • Интегрированное бизнес-планирование (IBP)
    • Хранилища данных (DWH / Lakehouse)
    • Каталоги данных (Data Catalog)
    • Системы ETL и ELT
    • AI / Исскуственный интеллект
    • Шина данных (ESB)
    • Система управления мастер-данными (MDM)
    • Семантический слой
  • Услуги
    • Переход на отечественные BI и DWH системы
    • Консалтинг
    • Пилотный проект
    • Обучение и сертификация
    • Бесплатное обучение
    • Поддержка
    • Технические задания
    • Сбор требований для проекта внедрения BI-системы
    • CI/CD для DWH
    • Аудит BI приложений и DWH
    • Выделенная команда
    • Настойка и поддержка баз данных
    • Разработка BI Стратегии
    • Styleguide для BI-системы
    • Как выбрать BI-систему
  • Курсы
    • Учебный курс Информационная грамотность (Data Literacy)
    • Учебный курс для бизнес-аналитиков
    • Учебный курс для системных аналитиков
    • Учебный курс по Data Governance
    • Учебный курс Как стать CDO
    • Учебный курс Современная архитектура хранилища данных
    • Учебный курс по Fine BI
    • Учебный курс по FineReport
    • Учебный курс по DWH
    • Учебный курс по Data Science (ML, AI)
    • Учебный курс по PostgreSQL
    • Учебный курс по Greenplum
    • Учебный курс по Apache Airflow и NiFi
    • Учебный курс по Open-source BI
    • Учебный курс по ClickHouse
    • Учебный курс по DataLens
    • Учебный курс по Loginom
    • Учебный курс по Modus BI и ETL
    • Учебный курс по Visiology
    • Учебный курс по dbt (Data Build Tool)
  • Компания
    • Руководство
    • Новости
    • Клиенты
    • Карьера
    • Скачать
    • Контакты

BI

  • FineBI
  • FineReport
  • FineDataLink
  • FineChatBI (FineAI)
  • Коннекторы данных из 1С в BI
  • Airflow / Nifi
  • Visiology
  • PIX BI
  • Modus BI
  • Yandex.DataLens
  • Open-source BI: Superset/Metabase
  • Luxms BI
  • AW BI + Alpha BI
  • FlyBI + Форсайт. Аналитическая Платформа
  • Loginom
  • Триафлай
  • AI / Исскуственный интеллект
  • Optimacros
  • Навигатор BI
  • Семантический слой

СУБД

  • Arenadata
  • ClickHouse
  • Greenplum
  • Postgres Professional
  • TData

Другое

  • Построение Data Platform
    • Аналитическое хранилище данных
    • Data Lake и Data Engineering
    • Подробнее про Data Lake
    • Внедрение Lakehouse
      • Apache Doris
      • StarRocks
      • Trino
    • Миграция витрин из пропиетарных DWH на новый стек
    • Учебный курс "Современная архитектура хранилища данных"
Главная » Курсы по системам бизнес-анализа и методологии » Учебный курс Современная архитектура хранилища данных » Опасные JOIN запросы которые незаметно замедляют вашу базу данных и как с этим бороться

Опасные JOIN запросы которые незаметно замедляют вашу базу данных и как с этим бороться

Казалось бы, что может быть проще, чем объединить несколько таблиц? Однако на практике именно в этих, на первый взгляд, безобидных операциях скрываются самые коварные и дорогостоящие ошибки. Неоптимальный JOIN может не проявлять себя на тестовых наборах данных, но когда объем информации вырастает до миллионов записей, он моментально превращается в узкое горлышко, способное остановить работу критически важных бизнес-процессов.

Сегодня мы детально разберем самые распространенные и опасные антипаттерны использования JOIN. Для каждого случая мы не только покажем пример ошибки, но и объясним, что именно происходит в недрах СУБД, почему это приводит к катастрофическим последствиям, предложим конкретные шаги по исправлению и расскажем, в каких редких случаях такое решение может быть допустимо.

Все примеры приведены для PostgreSQL, но подавляющее большинство рассмотренных проблем в равной степени актуальны и для MySQL, SQL Server и других популярных систем управления базами данных.

 

1. Декартово произведение: CROSS JOIN по ошибке

Представьте, что разработчик пишет запрос, чтобы получить список пользователей и их платежей. По старой привычке или невнимательности он использует синтаксис с запятой, забывая указать условие объединения ON.

SELECT  u.id, p.amount
 FROM    users u, payments p;      -- упс

 

База данных, не найдя условия для объединения таблиц, выполняет операцию CROSS JOIN, то есть декартово произведение. Каждая строка из таблицы users соединяется с каждой строкой из таблицы payments. Если в users 1 миллион записей, а в payments 2 миллиона, результирующий набор составит 2 триллиона строк. Это немедленно приводит к полному сканированию обеих таблиц, колоссальным затратам оперативной памяти и дискового пространства для временных данных, и, в конечном итоге, к полной остановке сервера или крайне длительному времени выполнения запроса. Риск заключается в том, что на тестовых стендах с малым объемом данных эта ошибка может остаться незамеченной, но при выходе на продакшен она проявится в самый неподходящий момент.

Всегда используйте явный синтаксис JOIN ... ON. Это не только предотвращает случайные декартовы произведения, но и делает код более читаемым и легким для поддержки.

SELECT u.id, p.amount
FROM users u
JOIN payments p ON u.id = p.user_id; -- Явное указание условия

 

Если же декартово произведение действительно требуется для вашей задачи, что бывает крайне редко, используйте ключевые слова CROSS JOIN и обязательно оставьте комментарий, объясняющий, почему это необходимо. Это предупредит других разработчиков о ваших намерениях.

 

 

2. JOIN с использованием функций в условии

Часто возникает необходимость объединить таблицы по полю, которое может быть записано в разном регистре, например, по электронной почте. Разработчик решает эту проблему, используя функцию LOWER.

SELECT  u.id, s.id
 FROM    users u
 JOIN    subscriptions s
         ON LOWER(u.email) = LOWER(s.email);

 

Любая функция, примененная к колонке в условии JOIN, делает ее непрозрачной для оптимизатора базы данных. Он больше не может использовать стандартный индекс по полю email, потому что индекс построен на исходных значениях, а не на их преобразованной версии. В результате СУБД вынуждена выполнять полное сканирование обеих таблиц, что при больших объемах данных может занимать десятки секунд вместо миллисекунд. Риск заключается в постепенном замедлении системы по мере роста данных, причем причина будет неочевидной для тех, кто не умеет читать планы запросов.

Создайте функциональный индекс, который будет построен на результате применения той же функции, что используется в запросе.

CREATE INDEX CONCURRENTLY idx_users_email_lower ON users (LOWER(email));
CREATE INDEX CONCURRENTLY idx_subscriptions_email_lower ON subscriptions (LOWER(email));

 

После создания таких индексов оптимизатор снова сможет использовать их для эффективного поиска, и скорость запроса восстановится. В качестве альтернативы можно заранее нормализовать данные, сохраняя их в нижнем регистре в отдельном поле, и создавать индекс уже по этому полю.

 

3. Неправильное использование LEFT JOIN с фильтрацией

Разработчик хочет получить список всех заказов, но при этом отфильтровать только те, по которым есть возвраты. Он использует LEFT JOIN, но добавляет условие на NULL для таблицы возвратов.

SELECT  o.id, r.id
 FROM    orders o
 LEFT JOIN refunds r ON r.order_id = o.id
 WHERE   r.id IS NOT NULL;         -- Моментально «съедает» все NULL'ы

 

Фильтр WHERE r.id IS NOT NULL после выполнения JOIN, фактически аннулирует смысл LEFT JOIN. Все строки из таблицы orders, для которых не нашлось соответствия в refunds, будут отброшены. В результате запрос выполняется дольше, так как СУБД сначала выполняет внешнее соединение, а затем отфильтровывает его результаты, и при этом вы получаете не тот результат, который ожидали. Риск двойной вы получаете неверные данные и тратите лишние вычислительные ресурсы.

Если ваша цель найти только те заказы, по которым есть возвраты, всегда используйте INNER JOIN. Это прямо выражает ваше намерение и позволяет оптимизатору выбрать более эффективный план запроса.

SELECT o.id, r.id
FROM orders o
JOIN refunds r ON r.order_id = o.id; -- Явный INNER JOIN

 

Если же вам нужен именно LEFT JOIN с определенными условиями для правой таблицы, переносите эти условия в секцию ON.

SELECT o.id, r.id
FROM orders o
LEFT JOIN refunds r ON r.order_id = o.id AND r.processed = TRUE; -- Условие внутри ON

 

Такой подход сохранит все заказы, а к тем, по которым есть возвраты, добавит информацию только о тех возвратах, которые обработаны.

 

4. Соединение по полям с разными типами данных

Одна таблица использует тип VARCHAR для идентификатора, а другая INT. При соединении разработчик надеется на неявное преобразование.

SELECT  c.id, o.id
 FROM    customers  c         -- id VARCHAR
 JOIN    orders     o         -- customer_id INT
         ON c.id = o.customer_id;

 

База данных вынуждена выполнять неявное преобразование типов. Чаще всего она приводит значения к тому типу, который имеет более высокий приоритет. В данном случае это может быть преобразование целочисленного customer_id к текстовому формату. Это преобразование делает невозможным использование индекса по полю customer_id, так как индекс построен на целых числах, а не на их текстовых представлениях. Запрос будет выполняться через полное сканирование таблицы. Риск заключается в том, что ошибка может долго оставаться незамеченной, а ее исправление на работающей системе потребует изменения схемы данных, что является сложной и рискованной операцией.

Приводите типы данных связанных полей к единому стандарту на уровне схемы базы данных. Это самое правильное и долгосрочное решение. На этапе разработки внедрите в процесс CI CD скрипты, которые будут проверять запросы на наличие неявных преобразований типов в условиях JOIN.

 

5. Использование оператора OR в условии JOIN

Иногда бизнес логика требует объединять таблицы по одному из нескольких возможных ключей.

SELECT  *
 FROM    payments p
 JOIN    invoices i
       ON (i.id = p.invoice_id OR i.external_id = p.invoice_external_id);

 

Оператор OR создает неконъюнктивное условие, что крайне сложно для оптимизатора. Оценка селективности такого условия резко падает, и СУБД часто отказывается от использования индексов, выбирая полное сканирование таблиц. План запроса может показать несколько полных сканирований и вложенных циклов, что является верным признаком проблемы.

Разбейте сложное условие с OR на несколько независимых запросов, объединенных с помощью UNION ALL.

SELECT *
FROM payments p
JOIN invoices i ON i.id = p.invoice_id
UNION ALL
SELECT *
FROM payments p
JOIN invoices i ON i.external_id = p.invoice_external_id;

 

Такой подход позволяет оптимизатору использовать индексы для каждого отдельного условия. В некоторых случаях могут помочь частичные индексы.

 

6. Не-саргабельные выражения, ухудшающие использование индексов

Разработчик хочет выбрать данные только за сегодняшний день, используя функцию DATE.

SELECT  *
 FROM    logs l
 JOIN    users u ON u.id = l.user_id
 WHERE   DATE(l.created_at) = CURRENT_DATE;   -- ах да, надо же «только за сегодня»

 

Применение функции DATE к колонке created_at делает невозможным использование индекса по этой колонке. База данных вынуждена выполнить полное сканирование таблицы logs, преобразовать каждое значение created_at в дату и только потом сравнить его с текущей датой. При больших объемах логов это приводит к колоссальным задержкам.

Замените использование функции на условие с диапазоном.

SELECT *
FROM logs l
JOIN users u ON u.id = l.user_id
WHERE l.created_at >= CURRENT_DATE
AND l.created_at < CURRENT_DATE + INTERVAL '1 day';

 

Такой запрос является саргируемым и позволяет эффективно использовать индекс по полю created_at. Альтернативой может быть создание функционального индекса по DATE(created_at), но это следует делать с осторожностью и пониманием ответственности.

 

7. JOIN без поддержки индексов

Простой запрос, соединяющий две большие таблицы, но в одной из них отсутствует индекс по столбцу соединения.

SELECT *
FROM big_table_a a
JOIN big_table_b b ON b.a_id = a.id;

 

При отсутствии индекса по b.a_id оптимизатор не может эффективно найти соответствующие строки в таблице big_table_b для каждой строки из big_table_a. Ему приходится выполнять полное сканирование и сортировку одной или обеих таблиц перед выполнением соединения методом слияния. Сложность такого алгоритма оценивается как O(n log n), что при больших n приводит к экспоненциальному росту времени выполнения.

Создайте недостающий индекс.

CREATE INDEX CONCURRENTLY idx_big_b_a_id ON big_table_b (a_id);

 

После создания индекса обязательно выполните команду ANALYZE для обновления статистики, иначе оптимизатор может не воспользоваться новым индексом, так как у него будет неактуальная информация о распределении данных.

 

8. Неправильное использование LATERAL JOIN и CROSS APPLY

Эти операторы мощны, но опасны. Типичная ошибка: использование LATERAL для получения последней записи связанной таблицы для каждой строки основной таблицы.

SELECT u.id, l.last_login
FROM users u
CROSS JOIN LATERAL (
SELECT last_login
FROM logins
WHERE user_id = u.id
ORDER BY created_at DESC
LIMIT 1
) l;

 

LATERAL JOIN выполняет подзапрос для каждой строки исходной таблицы. Для таблицы с 500 тысячами пользователей этот подзапрос будет выполнен 500 тысяч раз. Если внутри подзапроса есть сортировка, то затраты становятся просто астрономическими.

Используйте оконные функции или предварительную агрегацию.

SELECT DISTINCT ON (u.id)
u.id,
l.last_login
FROM users u
JOIN logins l ON l.user_id = u.id
ORDER BY u.id, l.created_at DESC;

 

Или с использованием агрегации в CTE

WITH last_logins AS (
SELECT user_id, MAX(created_at) as last_login
FROM logins
GROUP BY user_id
)
SELECT u.id, l.last_login
FROM users u
LEFT JOIN last_logins l ON l.user_id = u.id;

 

Оба этих подхода значительно эффективнее, так как избегают цикличного выполнения запроса.

 

9. Соединение с материализованными представлениями без индексов

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

SELECT *
FROM orders o
JOIN sales_report_monthly m ON m.customer_id = o.customer_id;

 

Материализованное представление sales_report_monthly может быть огромным и, что критично, не иметь индексов. JOIN с такой структурой происходит так же медленно, как и с обычной большой таблицей, сводя на нет всю потенциальную выгоду от предварительной агрегации.

Создавайте индексы по ключевым полям материализованных представлений, используемым в соединениях. В некоторых случаях более эффективно будет отказаться от материализованного представления в пользу инлайн агрегации или использования временных таблиц с нужными индексами на время выполнения серии запросов.

 

10. Множественное дублирование строк из за связи многие ко многим

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

SELECT p.id, t.tag
FROM products p
JOIN products_tags pt ON pt.product_id = p.id
JOIN tags t ON t.id = pt.tag_id;

 

Если один товар имеет несколько тегов, в результате он будет дублироваться в выходной выборке столько раз, сколько у него тегов. Это может быть неочевидно для разработчика API или фронтенда, который ожидает получить уникальные товары. В результате передаются избыточные данные, растет нагрузка на сеть, а клиентское приложение может работать некорректно.

Используйте агрегирующие функции для сборки тегов в массив или JSON объект прямо на стороне базы данных.

SELECT p.id, ARRAY_AGG(t.tag) as tags
FROM products p
JOIN products_tags pt ON pt.product_id = p.id
JOIN tags t ON t.id = pt.tag_id
GROUP BY p.id;

 

Такой подход гарантирует, что каждая строка результата будет соответствовать одному товару. Кроме того, убедитесь, что на таблицу связи products_tags наложено ограничение уникальности UNIQUE(product_id, tag_id), чтобы избежать дублирования связей на уровне данных.

JOIN операции - это не просто синтаксическая конструкция для объединения таблиц. Это сложный контракт между разработчиком и оптимизатором базы данных. Нарушение этого контракта ведет к прямому ущербу для бизнеса из-за замедления работы приложений, увеличения затрат на инфраструктуру и простоев.

Никогда не забывайте про ключевые принципы написания эффективных JOIN запросов:

  • Всегда проверяйте план запроса EXPLAIN ANALYZE для запросов, работающих с большими объемами данных;
  • Следите за тем, чтобы условия JOIN были проиндексированы и не скрыты внутри функций;
  • Явно указывайте тип JOIN INNER, LEFT, RIGHT, понимая семантику каждого;
  • Следите за согласованностью типов данных в соединяемых полях;
  • Избегайте сложных условий с OR внутри ON, разбивая их на UNION ALL.

 

Кроме того, мы настоятельно рекомендуем внедрить в процесс код ревью обязательную проверку SQL запросов, особенно тех, которые содержат JOIN операции. Регулярный аудит производительности базы данных и выявление медленных запросов помогут вовремя обнаружить и устранить проблемы, пока они не привели к серьезным последствиям.

Помните, что стоимость исправления ошибки в запросе на этапе разработки в десятки раз ниже, чем стоимость простоя бизнес критичного приложения в рабочем окружении. Будьте бдительны и пишите максимально эффективный код.

 

Узнать стоимость решенияЗапросить видео презентацию

← Предыдущая статья
Учебный курс по ZooKeeper
Следующая статья →
Picodata: перезагрузка in-memory баз данных для архитектуры будущего

Решения

Анализировать ФинансыУвеличивайте ПродажиОптимальный Склад и ЛогистикаМаркетинговые Метрики

Клиенты
  • ООО «Модум-Транс» — независимый оператор грузовых железнодорожных перевозок, лидирующий по количеству инновационного парка на сети РЖД.

  • СберКорус (Группа компаний Сбербанка) – это ИТ‑компания, ИТ‑интегратор, SaaS-провайдер. Является разработчиком цифровых сервисов и услуг для автоматизации широкого диапазона бизнес-процессов юридических лиц. В 2004 году компания стала первым в России оператором электронного документооборота, а в 2012 году вошла в экосистему Сбера. 

  • «Балтийский лизинг» — первая компания в России, получившая лицензию № 0001 от Министерства экономики РФ на лизинговую деятельность, лицензия зарегистрирована 2 сентября 1996 года. «Балтийский лизинг» работает на российском рынке 33 года: компания представлена 79 филиалами по всей стране, сегодня в штате более 1300 сотрудников. За последние десять лет компания профинансировала имущество для 80 000 клиентов.

  • Ручная обработка заявок на займы в МФО ДоброЗайм была малоэффективной и приводила к высоким затратам по ФОТ отдела верификации и андеррайтинга. При этом время обработки заявок было высоким, как и количество ошибок под влиянием человеческого фактора. Дополнительные сложности создавал сложный документооборот, обусловленный неконсолидированной кредитной историей и скоринговой оценкой. Все это суммарно мешало масштабированию бизнеса МФО.

  • Решения
    • Дистрибуция
    • Розничная торговля
    • Производство
    • Операторы связи
    • Страхование
    • Банки
    • Лизинг
    • Логистика
    • Нефтегазовый сектор
    • Медицина
    • Сеть ресторанов
    • E-Commerce
    • Энергетика
    • Фармацевтика
  • Услуги
    • Переход на отечественные BI и DWH
    • Консалтинг
    • Пилотный проект
    • Обучение и сертификация
    • Бесплатное обучение
    • Техническая поддержка
    • Технические задания
    • Сбор требований для проекта внедрения BI-системы
    • CI/CD для DWH
    • Аудит BI приложений
    • Выделенная команда
    • Настойка и поддержка баз данных
    • Разработка BI Стратегии
    • Styleguide для BI-системы
    • Как выбрать BI-систему
  • Платформы
    • FineBI
    • FineReport
    • FineDataLink
    • Коннекторы данных из 1С в BI
    • Airflow + NiFi
    • Visiology
    • Luxms BI
    • Modus BI
    • PIX BI
    • Arenadata
    • ClickHouse
    • Greenplum
    • Postgres Professional
    • Open-source BI: Superset/Metabase
    • Loginom
    • Yandex.DataLens
    • AI / Исскуственный интеллект
    • Optimacros
    • Шины данных
  • Курсы
    • Учебный курс Информационная грамотность
    • Учебный курс для бизнес-аналитиков
    • Учебный курс для системных аналитиков
    • Учебный курс по Data Governance
    • Учебный курс Как стать CDO
    • Учебный курс Современная архитектура хранилища данных
    • Учебный курс по Fine BI
    • Учебный курс по FineReport
    • Учебный курс по DWH
    • Учебный курс по Data Science (ML, AI)
    • Учебный курс по PostgreSQL
    • Учебный курс по Apache Airflow и NiFi
    • Учебный курс по Open-source BI
    • Учебный курс по ClickHouse
    • Учебный курс по DataLens
    • Учебный курс по Loginom
    • Учебный курс по Modus BI и ETL
    • Учебный курс по Visiology
    • Учебный курс по dbt
  • Функциональные решения
    • Создание Data Lake
    • Цифровая трансформация
    • Управление по KPI
    • Финансы
    • Продажи
    • Склад
    • HR
    • Маркетинг
    • Внутренний аудит
    • Категорийный менеджмент
    • S&OP и прогнозная аналитика
    • Геоаналитика
    • Цепочки поставок (SCM)
    • AutoML
    • Process Mining
    • Сквозная аналитика
  • Компания
    • О нас
    • Руководство
    • Новости
    • Клиенты
    • Скачать
    • Контакты
    • Политика конфиденциальности
RutubeVkontakteLinkedInYouTube
ООО "Би Ай Консалт",
ИНН: 7811437757,
ОГРН: 1097847154184
199178, Россия,
Санкт-Петербург,
6-ая линия В.О., Д. 63, 4 этаж
Тел: +7 (812) 334-08-01
Тел: +7 (499) 608-13-06
E-mail: info@biconsult.ru

 

 

 

 

 

×

Пользуясь сайтом, вы соглашаетесь с использованием cookies и политикой конфиденциальности.