Объяснить план Oracle с помощью статьи показывает слияние присоединиться к декартовой - PullRequest
0 голосов
/ 16 ноября 2018

Я пытаюсь улучшить производительность запроса, показанного ниже, переписав условное LEFT JOIN как UNION из INNER JOIN и базовую таблицу. Я использую Oracle 12c.

Соответствующие таблицы: ASSIGNMENTS и CLASSES. Требование заключается в том, что для заданного значения ITEM_ID нам необходимо извлечь подробности из соответствующих записей в двух таблицах.

  • Существует только одна CLASSES запись таблицы для каждого ITEM_ID.
  • Таблица ASSIGNMENTS может иметь или не иметь запись для любого данного ITEM_ID из таблицы CLASSES.
  • Детали записи ASSIGNMENTS требуются, только если запись CLASSES имеет enable_capacity = 'Y'.

Я пришел к следующему запросу:

WITH CLASSES_WITH_CAPACITY AS (
  SELECT maximum_attendees,  item_id,  enable_capacity
  FROM CLASSES classes WHERE enable_capacity = 'Y' AND classes.item_id = 123 
), CLASSES_WITHOUT_CAPACITY AS (
  SELECT maximum_attendees,  item_id,  enable_capacity
  FROM CLASSES classes WHERE enable_capacity is null AND item_id = 123 
) 
SELECT maximum_attendees, item_id, max_position_value, 
  enable_capacity, enable_waitlist FROM (
    (SELECT classes.maximum_attendees, classes.item_id,
      MAX(assignments.wait_position) max_position_value,
      classes.enable_capacity, classes.enable_waitlist
     FROM CLASSES_WITH_CAPACITY classes 
      JOIN WLF_ASSIGNMENT_RECORDS_F assignments ON (classes.item_id = assignments.item_id) 
      WHERE ( assignments.status <> 'EXPIRED')
      GROUP BY classes.item_id, classes.maximum_attendees, 
      classes.enable_capacity, classes.enable_waitlist )
    UNION ALL
    (SELECT classes.maximum_attendees, classes.item_id, 
      null AS max_position_value, 
      classes.enable_capacity, classes.enable_waitlist
      FROM CLASSES_WITHOUT_CAPACITY classes
    )
   );

По сути, операторы предложения WITH будут взаимоисключающими, то есть только один из запросов предложения WITH будет иметь одну запись CLASSES с заданным ITEM_ID. Ниже приведен вывод EXPLAIN PLAN для вышеуказанного запроса:

-------------------------------------------------------------------------------------------------------------
| Id  | Operation                                | Name             | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                         |                  |     2 |   244 |     8  (13)| 00:00:01 |
|   1 |  VIEW                                    |                  |     2 |   244 |     8  (13)| 00:00:01 |
|   2 |   UNION-ALL                              |                  |       |       |            |          |
|   3 |    HASH GROUP BY                         |                  |     1 |   111 |     6  (17)| 00:00:01 |
|   4 |     MERGE JOIN CARTESIAN                 |                  |     1 |   111 |     5   (0)| 00:00:01 |
|*  5 |      TABLE ACCESS BY INDEX ROWID         | ASSIGNMENTS      |     1 |    75 |     4   (0)| 00:00:01 |
|*  6 |       INDEX RANGE SCAN                   | ASSIGNMENTS_N9   |     9 |       |     1   (0)| 00:00:01 |
|   7 |      BUFFER SORT                         |                  |     1 |    36 |     2  (50)| 00:00:01 |
|*  8 |       TABLE ACCESS BY INDEX ROWID BATCHED| CLASSES          |     1 |    36 |     1   (0)| 00:00:01 |
|*  9 |        INDEX RANGE SCAN                  | CLASSES_N2       |     1 |       |     0   (0)| 00:00:01 |
|* 10 |    TABLE ACCESS BY INDEX ROWID BATCHED   | CLASSES          |     1 |    36 |     2   (0)| 00:00:01 |
|* 11 |     INDEX RANGE SCAN                     | CLASSES_N2       |     1 |       |     1   (0)| 00:00:01 |
-------------------------------------------------------------------------------------------------------------

Теперь меня беспокоит вопрос о типе соединения MERGE JOIN CARTESIAN. Похоже, что он использует MERGE JOIN CARTESIAN для INNER JOIN между CLASSES и ASSIGNMENTS.

У меня первоначальное впечатление, что это не приведет к снижению производительности, только потому, что объединение будет выполняться только для одной записи. Это правильно?

В чем причина использования Cartesian Join Type вообще?

Учитывая тот факт, что тип Cartesian Join используется, когда предикаты JOIN отсутствуют, и / или объединенные предикаты не соответствуют надлежащим отношениям первичного-внешнего ключа, если оптимизатор не находит индексы по псевдониму CLASSES ? То есть, использование предложения WITH скрывает индексы в таблице внутри предложения WITH при дальнейшем присоединении к предложению WITH?

Если это так, не следует ли использовать предложение WITH - особенно, если к выходу предложения WITH дополнительно присоединяется?

Также приветствуются любые другие предложения относительно моего подхода. Спасибо!

1 Ответ

0 голосов
/ 17 ноября 2018

Почему используется CARTESIAN JOIN?

Как вы указали, в таблице не более одной записи CLASSES с данным item_id.Oracle понимает это (см. Столбец Rows в строке 9 плана выполнения) и решает сделать декартово объединение этой одной строки с ожидаемыми 9 строками таблицы ASSIGNMENTS.

Это прекрасно работает , если оценка мощности в порядке, но может вызвать большие проблемы, если она неточна.Я полагаю, что item_id не поддерживается первичным ключом или уникальным индексом - см. INDEX RANGE SCAN в строке 9;Я бы ожидал INDEX UNIQUE SCAN в случае уникального индекса.Так что это потенциальная причина проблем.

Использование WITH предложений

Как уже упоминалось, предложения WITH в вашем запросе не действуют, нотакже не причиняет вреда.Как вы видите в плане выполнения, они объединены в основном запросе.Oracle переписывает запрос и устраняет оба подзапроса.

Повышение производительности

Вы не указали, к каким проблемам вы пытались обратиться, исключив LEFT OUTER JOIN;в любом случае может быть тонкая точка, в которой следует проявлять определенную осторожность.

В принципе, есть две возможности выполнить план

1) сначала присоединиться, а затем объединиться, чтобы получить MAX (wait_position)

2) сначала получить значение MAX, а затем соединить (тривиально) две таблицы из одной строки

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

Проблемы начинаются, если вы сталкиваетесь с назначением с тоннами строк для некоторых ITEM_ID.Это иногда называется умиранием в NESTED LOOPS , поскольку вы зацикливаетесь на миллионах строк только после этого, агрегируя результат в одну строку, содержащую MAX.

Oracle для этого случая.доступ к индексу INDEX RANGE SCAN (MIN/MAX), который получает максимальное значение непосредственно из индекса (без доступа к таблице и без сканирования всех значений - помните, что индекс отсортирован, поэтому получить значение MAX тривиально).

Итак, согласно предположениюиз этих индексов вы можете попробовать альтернативный запрос ниже

уникальный индекс по классам (item_id) индекс по WLF_ASSIGNMENT_RECORDS_F (item_id, wait_position)

Второй индекс важен, так как вы обойдететаблица вообще и вы ее до максимума (wait_position)

Запрос (упрощенный пропуском некоторых столбцов) выглядит следующим образом:

with max_wait as (
select  max(a.wait_position) max_wait_position
from assignments a
where a.item_id = 1)
select c.item_id, m.max_wait_position
from classes c
left outer join max_wait m
on  c.enable_capacity = 'Y'
where c.item_id = 1;

Обратите внимание, что подзапрос вычисляетmax(wait_position).На следующем шаге вы outer joins переводите таблицу в подзапрос, но только если c.enable_capacity = 'Y' - дальнейший предикат не требуется, так как оба источника строк имеют максимум одну строку.

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

----------------------------------------------------------------------------------------------------
| Id  | Operation                        | Name            | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                 |                 |     1 |    18 |     3   (0)| 00:00:01 |
|   1 |  NESTED LOOPS OUTER              |                 |     1 |    18 |     3   (0)| 00:00:01 |
|   2 |   TABLE ACCESS BY INDEX ROWID    | CLASSES         |     1 |     5 |     1   (0)| 00:00:01 |
|*  3 |    INDEX UNIQUE SCAN             | CLASSES_UX1     |     1 |       |     0   (0)| 00:00:01 |
|   4 |   VIEW                           | VW_LAT_1D42B1AA |     1 |    13 |     2   (0)| 00:00:01 |
|*  5 |    FILTER                        |                 |       |       |            |          |
|   6 |     VIEW                         |                 |     1 |    13 |     2   (0)| 00:00:01 |
|   7 |      SORT AGGREGATE              |                 |     1 |     9 |            |          |
|   8 |       FIRST ROW                  |                 |     1 |     9 |     2   (0)| 00:00:01 |
|*  9 |        INDEX RANGE SCAN (MIN/MAX)| ASSIGNMENTS_IX1 |     1 |     9 |     2   (0)| 00:00:01 |
----------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   3 - access("C"."ITEM_ID"=1)
   5 - filter("C"."ENABLE_CAPACITY"='Y')
   9 - access("A"."ITEM_ID"=1)

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

Последнее замечание. Я надеюсь, что в вашей производственной среде вы используете переменные связывания, а не буквальные ключи для item_id;)

...