Трендовые github проекты в нашем телеграм канале. Подпишись → Как автоматизировать разбор медленных запросов Oracle через LLM
Типичная история: запрос на тестовом стенде отдаёт результат мгновенно, а на проде начинает тормозить, причём иногда через полгода, когда таблица дорастает до миллионов строк. Многие команды в такой ситуации обвешивают приложение логированием и назначают дежурного, который эти логи читает. При этом в Oracle уже есть ASH (Active Session History) и AWR (Automatic Workload Repository): база сама записывает, какие сессии чем были заняты и какие SQL съели больше всего ресурсов.
Узкое место в ручном разборе — человек. Чтобы понять, почему план плохой, нужно знать схему, существующие индексы, партиционирование, статистику по колонкам. Всё это лежит в словаре данных той же базы, поэтому сбор контекста легко автоматизировать, а интерпретацию можно отдать LLM.
Что делает Optimus Oracle
Optimus Oracle — утилита на Python под лицензией MIT. Порядок работы:
- Забирает Top-20 самых тяжёлых запросов из ASH или AWR (число настраивается).
- Получает планы через
DBMS_XPLAN— из кэша курсоров в памяти или из историческойDBA_HIST_SQL_PLAN. - Собирает информацию об объектах, которые участвуют в запросе: размеры, столбцы, NDV, индексы.
- Достаёт DDL этих объектов.
- Отправляет всё в модель и получает рекомендации: создать индекс, переписать запрос, поменять способ хранения таблицы ради сокращения 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 или разработчиком.