Эта небольшая заметка вдохновлена реальной и гораздо более сложной проблемой, которую я наблюдал и исследовал в PL/SQL коде, написанном довольно опытным разработчиком в Лондоне на моём предыдущем месте работы.
Начиналось это как довольно банальное упражнение в performance tuning для SQL-кода, использующего иерархические запросы.
Исследование затруднялось тем, что изначально использовалось PARALLEL QUERY и PARALLEL DML, и вначале параллельность казалась главным подозреваемым :)
Однако после нескольких часов терзаний и подходов к снаряду я осознал, что это довольно общая проблема, которая может проявляться в разнообразных execution plans с использованием «connect by» — проприетарного синтаксиса Oracle для иерархических запросов.
Кстати, решение было скопипасчено из интернета для подобного класса задач и содержит эту «бомбу», привожу ссылки на статьи: решение с distinct или solution 2.
После осознания проблемы довольно нетрудно найти подходящее решение и избежать такого зловредного поведения. Кстати, сразу подскажу, что некоторые правильные подходы содержатся в комментариях к статьям, ссылки на которые приведены выше.
В своей статье я намеренно не буду рассказывать деталей реальной задачи, а также путей решения, вместо этого хочу сосредоточиться на сути проблемы и на том аспекте, который считаю весьма общим и, в каком-то смысле, показательным, раскрывающим некоторые внутренние детали исполнения таких SQL.
Здесь будет представлен простейший синтетический пример, демонстрирующий проблему на простейшем запросе с использованием dual.
Дисклеймер: не запускайте низлежащий запрос на высоконагруженных базах Oracle.
select distinct id
from (select id
from (select 1 id
from dual
union all
select 2
from dual
union all
select 3
from dual)
connect by level < 1e6)
И давайте сразу посмотрим на план исполнения запроса:
SQL_ID dzqk3jmt0xtj6, child number 0
-------------------------------------
select distinct id from ( select id from ( select 1 id from dual union
all select 2 from dual union all select 3 from dual ) connect by level
<1e6)
Plan hash value: 318940406
---------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | | 7 (100)| |
| 1 | HASH UNIQUE | | 3 | 9 | 7 (15)| 00:00:01 |
| 2 | VIEW | | 3 | 9 | 6 (0)| 00:00:01 |
| 3 | CONNECT BY WITHOUT FILTERING| | | | | |
| 4 | VIEW | | 3 | 9 | 6 (0)| 00:00:01 |
| 5 | UNION-ALL | | | | | |
| 6 | FAST DUAL | | 1 | | 2 (0)| 00:00:01 |
| 7 | FAST DUAL | | 1 | | 2 (0)| 00:00:01 |
| 8 | FAST DUAL | | 1 | | 2 (0)| 00:00:01 |
---------------------------------------------------------------------------------------
Теперь попробуем получить SQL-Monitor отчёт после некоторого времени ожидания.
select dbms_sqltune.report_sql_monitor(sql_id =>'dzqk3jmt0xtj6',type => 'TEXT') from dual;
Результат будет похож на:
SQL Monitoring Report
SQL Text
------------------------------
select distinct id from ( select id from ( select 1 id from dual union all select 2 from dual union all select 3 from dual ) connect by level <1e6)
Global Information
------------------------------
Status : EXECUTING
Instance ID : 1
Session : RLS_USER (355:26535)
SQL ID : dzqk3jmt0xtj6
SQL Execution ID : 16777216
Execution Started : 03/05/2023 16:55:23
First Refresh Time : 03/05/2023 16:55:29
Last Refresh Time : 03/05/2023 18:38:19
Duration : 6178s
Module/Action : PL/SQL Developer/Secondary Session
Service : XXXXXZZZZ
Program : plsqldev.exe
Global Stats
================================
| Elapsed | Cpu | Other |
| Time(s) | Time(s) | Waits(s) |
================================
| 6176 | 6167 | 8.61 |
================================
SQL Plan Monitoring Details (Plan Hash Value=318940406)
===============================================================================================================================================
| Id | Operation | Name | Rows | Cost | Time | Start | Execs | Rows | Mem | Activity | Activity Detail |
| | | | (Estim) | | Active(s) | Active | | (Actual) | | (%) | (# samples) |
===============================================================================================================================================
| 0 | SELECT STATEMENT | | | | | | 1 | | | | |
| 1 | HASH UNIQUE | | 3 | 7 | 6135 | +2 | 1 | 0 | 760K | 10.48 | Cpu (645) |
| 2 | VIEW | | 3 | 6 | 5941 | +6 | 1 | 4G | | 1.33 | Cpu (82) |
| -> 3 | CONNECT BY WITHOUT FILTERING | | | | 6177 | +1 | 1 | 4G | | 88.18 | Cpu (5426) |
| 4 | VIEW | | 3 | 6 | 1 | +6 | 1 | 3 | | | |
| 5 | UNION-ALL | | | | 1 | +6 | 1 | 3 | | | |
| 6 | FAST DUAL | | 1 | 2 | 1 | +6 | 1 | 1 | | | |
| 7 | FAST DUAL | | 1 | 2 | 1 | +6 | 1 | 1 | | | |
| 8 | FAST DUAL | | 1 | 2 | 1 | +6 | 1 | 1 | | | |
===============================================================================================================================================
Что мы можем заметить из отчёта выше:
Запрос выполняется почти 2 часа и, в основном, занимается тем, что пожирает CPU.
Мы запроцессили порядка 4G (4,000,000,000,000) строк (и не собираемся на этом останавливаться).
Мы использовали 0 TEMPа и какие-то совсем незначительные 760K PGA.
Некоторые выводы:
Внутренняя часть плана генерит бесконечный набор данных (из-за потенциальной ошибки кодирования).
Если бы не HASH UNIQUE шаг в execution plan, мы бы довольно быстро упали, отъев всю доступную для сеанса PGA. Однако наличие HASH UNIQUE приводит к тому, что внутренняя часть плана постоянно «кормит» вышестоящий шаг плана данными и откидывает большую часть этих данных на лету, все это и приводит к бесконечному выполнению запроса.
P.S. Комбинация из CONNECT BY и какого-либо варианта UNIQUE (HASH UNIQUE/SORT UNIQUE) следующим шагом — является потенциально опасной и может приводить к выполнению запросов, которые впустую пожирают системные ресурсы.
P.P.S. Возможно, вы найдёте варианты положительного использования данного «искусственного», но очень интересного по поведению запроса.