Logo Craft Homelab Docs Нейросети Хостинг Обучение Контакты
Optimus Oracle: тяжёлые запросы из ASH/AWR и рекомендации по индексам от LLM Трендовые github проекты в нашем телеграм канале. Подпишись →
20 октября 2026 г.

Как автоматизировать разбор медленных запросов Oracle через LLM

Типичная история: запрос на тестовом стенде отдаёт результат мгновенно, а на проде начинает тормозить, причём иногда через полгода, когда таблица дорастает до миллионов строк. Многие команды в такой ситуации обвешивают приложение логированием и назначают дежурного, который эти логи читает. При этом в Oracle уже есть ASH (Active Session History) и AWR (Automatic Workload Repository): база сама записывает, какие сессии чем были заняты и какие SQL съели больше всего ресурсов.

Узкое место в ручном разборе — человек. Чтобы понять, почему план плохой, нужно знать схему, существующие индексы, партиционирование, статистику по колонкам. Всё это лежит в словаре данных той же базы, поэтому сбор контекста легко автоматизировать, а интерпретацию можно отдать LLM.

Что делает Optimus Oracle

Optimus Oracle — утилита на Python под лицензией MIT. Порядок работы:

  1. Забирает Top-20 самых тяжёлых запросов из ASH или AWR (число настраивается).
  2. Получает планы через DBMS_XPLAN — из кэша курсоров в памяти или из исторической DBA_HIST_SQL_PLAN.
  3. Собирает информацию об объектах, которые участвуют в запросе: размеры, столбцы, NDV, индексы.
  4. Достаёт DDL этих объектов.
  5. Отправляет всё в модель и получает рекомендации: создать индекс, переписать запрос, поменять способ хранения таблицы ради сокращения I/O.

К базе утилита ходит только с SELECT по системным представлениям и словарю. Никаких INSERT/UPDATE/DELETE/MERGE и DDL она не выполняет — даже предложенный индекс создаёт человек.

Модели подключаются тремя способами:

  • локально через Ollama;
  • через библиотеку openai для Python — это покрывает большинство облачных провайдеров с OpenAI-совместимым API;
  • через dashscope для Alibaba Cloud.

Запуск

Нужны Python 3.10+, доступ к экземпляру Oracle с включёнными ASH/AWR и либо API-ключ облачной модели, либо работающий Ollama.

git clone https://github.com/aleksanderFox/optimusOracle
cd optimusOracle
pip install -r requirements.txt
python oracle_query_optimizer.py

Перед запуском в config прописываются строка подключения к Oracle и LLM-провайдер.

Пример 1: функциональный индекс под реальные предикаты

Справочная таблица LIST_CONST — 7751 строка, 382 блока, около 3 МБ. Запрос фильтрует по UPPER(NAME), по диапазону :B3 BETWEEN FD AND TD и по комбинациям NVL(PARAM,0) / NVL(PARAM2,0), а потом сортирует через ORDER BY CASE, зависящий от bind-переменных.

В плане — TABLE ACCESS FULL, 247 buffers, 245 physical reads, 0,14 с. Для разового вызова это терпимо, для OLTP-запроса, который дёргается тысячи раз, — нет.

Модель (qwen3.8-max) правильно разобрала, почему существующие индексы не подходят:

  • (UPPER(NAME), FD, TD, PARAM) содержит PARAM, а запрос сравнивает NVL(PARAM,0); PARAM2 в индексе нет вообще;
  • уникальный индекс построен по LOWER(NAME), а запрос фильтрует по UPPER(NAME).

Ещё она отметила, что избавиться от сортировки индексом без изменения логики нельзя, и что сортировка здесь дешёвая. Предложение:

CREATE INDEX LIST_CONST_NAME_FD_TD_PAR2 ON LIST_CONST (
  UPPER(NAME), FD, TD, NVL(PARAM, 0), NVL(PARAM2, 0), VAL
) TABLESPACE INDX ONLINE;

VAL в конце делает индекс покрывающим: к таблице обращаться не нужно. После создания план сменился на INDEX RANGE SCAN, 2 buffers и 2 reads, время 0,01 с. Запрос стал быстрее в 14 раз, физических чтений — в 122 раза меньше.

Замеры делались на Oracle 11.2.0.4 с хинтом GATHER_PLAN_STATISTICS и после ALTER SYSTEM FLUSH BUFFER_CACHE. На проде сессии работают без STATISTICS_LEVEL = ALL, поэтому модель видит только оценки оптимизатора (E-Rows, Cost, E-Time), без фактических A-Rows. Если оценки сильно врут из-за устаревшей статистики, рекомендации тоже будут мимо.

Пример 2: склейка десяти таблиц и поиск по BLOB

Второй запрос тяжелее: десять таблиц через outer join в старом синтаксисе (+), поиск подстроки в BLOB через DBMS_LOB.INSTR(KK_CONVERT.BLOB_TO_CLOB(V.VAL_BLOB), :B1) и условие соединения N.N = CASE WHEN ... END. Время — 1 минута, 12M buffers, 327K reads. В плане полные сканы CLIENT_PROP_VALUES (3,5 млн строк, 885 МБ), ST_BAR_JOBS (10,5 млн), ST_DOC_PACK_DET (3,2 млн), ST_DOC_IN_DET и NOMENKLATURA.

Модель выделила пять проблем:

  • индекс CLIENT_PROP_VALUES_I9 (CLIENT_PROP_ITEMS_N, TD, FD) есть, но оптимизатор его игнорирует из-за функции над BLOB в WHERE;
  • BLOB конвертируется в CLOB и разбирается дважды — в WHERE и в скалярном подзапросе;
  • по содержимому LOB обычный B-tree не ищет;
  • CASE в условии соединения мешает выбрать порядок join и использовать индексы;
  • для каждого из трёх типов записей (324, 325, 511) нужна своя пара таблиц, а соединяются все шесть сразу, что может ещё и размножить строки.

Рекомендация — разбить запрос на три ветки UNION ALL, вынести фильтрацию и конвертацию BLOB в CTE и расширить индекс по CLIENT_PROP_VALUES колонкой CLIENT_N. После переписывания: 29,9 с вместо 60, reads 122K вместо 327K. Полные сканы ST_DOC_PACK_DET и ST_BAR_JOBS ушли, но по ST_DOC_IN_DET, ST_DOC_PACK и CLIENT_PROP_VALUES остались.

В новом плане CLIENT_PROP_VALUES читается трижды: CTE используется в каждой ветке UNION ALL, а оптимизатор не материализовал его сам. Это лечится хинтом /*+ MATERIALIZE */, но модель до этого не дошла. Дальше запрос пришлось дожимать вручную. Двукратное ускорение — хорошая отправная точка, финальным вариантом его считать нельзя.

Локальные модели против облачных

На ноутбуке с 16 ГБ памяти запускались qwen3:14b и gemma4:26b. С простыми случаями они справлялись. Например, gemma4:26b верно нашла причину 20-минутной выборки по BI_SHIPMENTS_RESERVE: строковые поля объявлены как CHAR (СКЛАД CHAR(1000), КОДНОМЕНКЛАТУРЫ CHAR(200)), каждое значение добивается пробелами до полной длины, средняя длина строки — 1483 байта. В итоге таблица занимает 3 ГБ и 390 041 блок, хотя полезных данных в ней на несколько сотен мегабайт.

На сложных запросах локальные модели галлюцинировали: предлагали использовать таблицы, которых в базе нет. Облачный qwen3 от Alibaba Cloud разбирал сложные планы заметно лучше и вдобавок советовал проверить, используются ли индексы, похожие на только что созданные, чтобы лишние удалить.

КритерийЛокальные (Ollama)Облачные (OpenAI, Alibaba)
Данные покидают периметрНетДа
Качество на сложных запросахСреднееВысокое
СтоимостьЖелезо и электричествоОплата за токены
СкоростьЗависит от GPUОбычно выше
Юридические рискиМинимальныеЗависят от политики провайдера

Если локальная модель — единственный допустимый вариант, берите квантование с высокой разрядностью, чтобы меньше терять в качестве ответов.

Что уходит из базы

Утилита только читает, но передаёт в модель очень много:

  • тексты SQL с именами схем, таблиц и колонок, а также литералами, которые в проде так и не заменили на bind-переменные: номера договоров, ИНН, e-mail;
  • планы выполнения — по ним видно, как связаны таблицы, какие есть индексы и партиции, сколько примерно строк;
  • статистику (NUM_DISTINCT, AVG_ROW_LEN, гистограммы), по которой восстанавливается распределение значений;
  • метаданные: описания таблиц, индексов, ограничений;
  • бизнес-логику в литералах CASE — в примере выше это названия операций «Приход», «Упаковка», «Подборка».

С облачной моделью всё это оседает в логах провайдера. Обещание не обучаться на данных не отменяет того, что они уже ушли по сети. Рабочая схема для продакшена: для чувствительных баз — изолированный контур с локальной LLM, облако — только для песочниц и обезличенных данных.

Как пользоваться рекомендациями

Модель уверенно предлагает индекс на несуществующую таблицу или колонку, которой нет в схеме. Поэтому каждую рекомендацию проверяйте на копии базы с GATHER_PLAN_STATISTICS и сравнивайте buffers и reads до и после. Индекс создавайте с ONLINE, как в первом примере, а соседние индексы с похожим набором колонок проверьте на использование и лишние удалите. Утилита сама ничего не применяет, и это правильное решение: она сокращает время на сбор контекста и первую гипотезу, а решение о том, что катить в прод, остаётся за DBA или разработчиком.