Bollywood HungamaDil Raju calls Ranbir Kapoor ‘Ranveer Singh’ at Ramayana’s grand Hyderabad event; immediately apologizes: “Sorry, Ranbir Kapoor garu!”PunchUK unemployment rate drops unexpectedly to 4.7%The Jerusalem PostAttack on Australian politician follows years of threats to public figuresRTP DesportoSeleção. Cristiano Ronaldo com regresso marcado para novembroInquirerUnderwater search is on for Bukidnon man swept off bamboo raftCNN TürkEuroLeague'de Türk derbisi haftasıDaily MaverickWHAT’S COOKING: Pan-fried duck breasts with blueberry-cherry sauce and a touch of ginger beerZDF heuteAktuelle Pressemitteilungen des ZDFUOLPapa retoma audiência após cirurgia contra câncer no pulmãoCollider'Clayface' Director Confirms James Gunn Gave Him Complete Freedom To Reinvent GothamSouth China Morning PostEnergy shock could spark Asia’s next debt crisisThe Japan Times‘Two Cigarettes, Three Nights’ explores messy emotions and moral compromises
The Daily Newsstand · Free, Always
Tuesday, October 20, 2026

Поиск узких мест в Oracle с помощью ASH/AWR и LLM

Translate

Добрый день, всем любителям Oracle и пирогов из картофельных очистков*!

I. Введение.

Одна из самых часто встречающихся проблем при использовании баз данных - это поиск узких мест: нахождение долго выполняющихся запросов и их исправление.

Как часто вы сталкивались с ситуацией, когда запрос мгновенно отрабатывает в тестовом окружении, но начинает «тормозить» на боевом сервере? Или замедление запросов проявляется не сразу, а по мере роста данных?

В одной организации, где я раньше работал, для поиска проблемных запросов использовалось логирование. Не хватает информации - нужно добавить логирования. И ввести дежурство по разбору логов. Ну как тут не вспомнить Томаса Кайта? «Не изобретайте колесо. Я видел не одну команду разработчиков, которые сталкивались с проблемами - не только на техническом уровне - из-за неосведомленности о том, что свободно предлагается Oracle. Это происходит, когда кто-то обнаруживает, что средство, на реализацию которого потрачено пару месяцев, уже есть в ядре базы данных»**. Зачем изобретать логирование, если в Oracle уже есть такие прекрасные средства, как ASH (Active Session History) и AWR (Automatic Workload Repository) - встроенные инструменты мониторинга и диагностики производительности?

Ручной анализ плана выполнения SQL-запросов, который можно получить из ASH или AWR, занимает время и требует экспертных знаний о работе конкретной базы данных Oracle, информации о схеме данных, наличии индексов, о разбиении таблиц на партиции и т.д. Но все необходимые для оптимизации данные уже есть в самой базе - это приводит к логическому выводу: процесс оптимизации можно автоматизировать.

Программа по оптимизации запросов в Oracle должна выполнять следующие действия:

  • Получать Top-20 (настраивается) самых тяжелых запросов из ASH или AWR;

  • Получать планы SQL-запросов через DBMS_XPLAN, из кэша курсоров в оперативной памяти или из исторической статистики DBA_HIST_SQL_PLAN;

  • Собирать информацию об объектах, используемых в SQL-запросах;

  • Получать DML-команды объектов, используемых в запросах.

Объединив данные, мы можем, используя экспертные системы на основе LLM моделей, получить рекомендации по оптимизации запросов:

  • Создание индексов;

  • Модификация запроса (если это возможно);

  • Изменение способа хранения в таблицах, которые могут привести к оптимизации операций ввода-вывода.

II. Инструмент.

Хочу поделиться опытом, представить вам свою программу: Optimus Oracle.

  • Программа позволяет подключать разнообразные LLM-модели:

    1. Локальные, используя сервер Ollama;

    2. Облачные - используя универсальную библиотеку OpenAI в Python для подключения к множеству облачных LLM сервисов или библиотеку Dashscope, для подключения к API-платформе от Alibaba Cloud

  • Программа выполняет только SELECT-запросы к системным представлениям и словарям Oracle, не используя DML INSERT/UPDATE/DELETE/MERGE или DDL;

  • Код программы выложен в свободном доступе на GitHub https://github.com/aleksanderFox/optimusOracle - под MIT лицензией, вы свободно можете ознакомиться с кодом и модифицировать программу под свои требования;

  • Не ругайте слишком сильно - программа написана на Python, а не на asm с xor AX, AX.

III. Быстрый старт.

IV. Примеры использования.

Приведу несколько примеров того, как использование программы существенно ускорило запросы.

Запрос:

SELECT
	VAL
FROM
	LIST_CONST
WHERE
	UPPER(NAME) = UPPER(:B4)
	AND :B3 BETWEEN FD AND TD
	AND (NVL(PARAM,0) = NVL(:B2 ,0) AND NVL(PARAM2,0) = NVL(:B1 ,0) OR
		NVL(PARAM,0) = NVL(:B2 ,0) AND NVL(PARAM2,0) = 0 OR
		NVL(PARAM,0) = 0 AND NVL(PARAM2,0) = NVL(:B1 ,0) OR NVL(PARAM,0) = 0 AND
		NVL(PARAM2,0) = 0 )
ORDER BY
	CASE
		WHEN NVL(PARAM,0) = NVL(:B2 ,0) AND NVL(PARAM2,0) = NVL(:B1 ,0) THEN 1
		WHEN NVL(PARAM,0) = NVL(:B2 ,0) AND NVL(PARAM2,0) = 0 THEN 2 
		WHEN NVL(PARAM,0) = 0 AND NVL(PARAM2,0) = NVL(:B1 ,0) THEN 3
		WHEN NVL(PARAM,0) = 0 AND NVL(PARAM2,0) = 0 THEN 4
	END

План (план получен с GATHER_PLAN_STATISTICS после сброса buffer cache - см. Примечание):

| Id  | Operation           | Name       | E-Rows | A-Rows | A-Time       | Buffers | Reads |
|-----|---------------------|------------|--------|--------|--------------|---------|-------|
| 0   | SELECT STATEMENT    |            |        | 1      | 00:00:00.14  | 247     | 245   |
| 1   | SORT ORDER BY       |            | 1      | 1      | 00:00:00.14  | 247     | 245   |
|* 2  | TABLE ACCESS FULL   | LIST_CONST | 1      | 1      | 00:00:00.14  | 247     | 245   |

Рекомендации qwen3.8-max*** (далее текст написан ИИ)

Узкие места:

1. Полное сканирование таблицы LIST_CONST

Таблица небольшая: 7751 строка, 382 блока, примерно 3 МБ.

Для редких выполнений TABLE ACCESS FULL здесь не является катастрофой.

Но если запрос выполняется часто, например в OLTP-режиме, полное сканирование каждый раз читает/проверяет все блоки таблицы, хотя по предикатам можно отсечь очень малую часть данных.

2. Фильтры не полностью поддерживаются существующими индексами

В запросе используются:

UPPER(NAME) = UPPER(:B4)

:B3 BETWEEN FD AND TD

NVL(PARAM,0) = NVL(:B2,0)

NVL(PARAM2,0) = NVL(:B1,0)

Существующий индекс: LIST_CONST_NAME_FD_TD_PAR (UPPER(NAME), FD, TD, PARAM) может помочь только частично с UPPER(NAME) и FD, но:

он содержит PARAM, а не NVL(PARAM,0);

он не содержит PARAM2;

он не покрывает предикаты по NVL(PARAM2,0).

Существующий уникальный индекс не подходит для этого SQL

Индекс:

LIST_CONST_NAME_TD_PARAM (LOWER(NAME), TD, NVL(PARAM,0), NVL(PARAM2,0))

не может быть напрямую использован, так как запрос фильтрует по UPPER(NAME), а не по LOWER(NAME), и дополнительно использует FD.

Сортировка не является проблемой: SORT ORDER BY обрабатывает оценочно 41 строку (реально одну - см. примечание). Даже если сортировка останется, её стоимость мала.

Избежать сортировки индексом без изменения логики запроса практически невозможно, так как ORDER BY CASE зависит от значений bind-переменных.

Рекомендация: создать функциональный составной индекс под предикаты запроса

Тип оптимизации: индекс.

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

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;

Конец блока, сформированного ИИ

После создания индекса план изменился:

| Id  | Operation          | Name                        | E-Rows | A-Rows | A-Time       | Buffers | Reads |
|-----|--------------------|-----------------------------|--------|--------|--------------|---------|-------|
| 0   | SELECT STATEMENT   |                             |        | 1      | 00:00:00.01  | 2       | 2     |
| 1   | SORT ORDER BY      |                             | 1      | 1      | 00:00:00.01  | 2       | 2     |
|* 2  | INDEX RANGE SCAN   | LIST_CONST_NAME_FD_TD_PAR2  | 1      | 1      | 00:00:00.01  | 2       | 2     |

Увеличение быстродействия: в 14 раз, количество Reads уменьшилось в 122 раза.

Небольшое примечание:

Нужно признаться честно, при эксплуатации боевого сервера все сессии запускаются без STATISTICS_LEVEL = ALL (или использовании хита GATHER_PLAN_STATISTICS в SQL-запросах). Как следствие план выполнения запроса не содержит столбцов A-Rows/A-Time/etc. Поэтому текст, сформированный LLM, основан на основании столбцов E-Rows (Estimated Rows), Cost, Time (E-Time). Для анализа результативности изменений, предложенного LLM, была собрана полная статистика с использованием хита GATHER_PLAN_STATISTICS. Для чистоты эксперимента перед выполнением команд выполнялась команда очистки данных из кэша буферов.

ALTER SYSTEM FLUSH BUFFER_CACHE;

Тестирование проводилось в Oracle 11g Enterprise Edition Release 11.2.0.4.0.

Но это простой запрос. Рассмотрим более сложный запрос:

SELECT
	ROWNUM AS RN, (
	SELECT
		STRAGG(VALUE(T))
	FROM
		TABLE(KK_ASB_ST_DOC.CLOB_TO_TABLE(KK_CONVERT.BLOB_TO_CLOB(V.VAL_BLOB))) T
	WHERE
		VALUE(T) LIKE '%' || :B1 || '%'
	) AS SN,
	V.FD, U.NAME AS PERSON_NAME,
	CASE
		WHEN V.CLIENT_PROP_ITEMS_N = 324 THEN INDOC.CONTRAGENT_N
		WHEN V.CLIENT_PROP_ITEMS_N = 325 THEN P.CONTRAGENT_N
		WHEN V.CLIENT_PROP_ITEMS_N = 511 THEN J.CONTRAGENT_N
	END AS CONTRAGENT_N,
	N.CODE AS NOM_CODE,
	CASE
		WHEN V.CLIENT_PROP_ITEMS_N = 324 THEN 'Приход'
		WHEN V.CLIENT_PROP_ITEMS_N = 325 THEN 'Упаковка'
		WHEN V.CLIENT_PROP_ITEMS_N = 511 THEN 'Подборка'
	END AS OPER_NAME,
	CASE
		WHEN V.CLIENT_PROP_ITEMS_N = 324 THEN INDOC.N
		WHEN V.CLIENT_PROP_ITEMS_N = 325 THEN P.N
		WHEN V.CLIENT_PROP_ITEMS_N = 511 THEN O.N
	END AS DOC_N,
	CASE
		WHEN V.CLIENT_PROP_ITEMS_N = 324 THEN INDOC.NUM_INVOICE
		WHEN V.CLIENT_PROP_ITEMS_N = 325 THEN P.DOC_NUM
		WHEN V.CLIENT_PROP_ITEMS_N = 511 THEN O.DOC
	END AS DOC
FROM
	CLIENT_PROP_VALUES V,
	ACTIONS A,
	PRM_USERS U,
	ST_DOC_IN_DET INDET,
	ST_DOC_IN INDOC,
	NOMENKLATURA N,
	ST_DOC_PACK_DET PD,
	ST_DOC_PACK P,
	ST_DOC_OUT O,
	ST_BAR_JOBS J
WHERE
	LENGTH(:B1 ) > 7
	AND V.CLIENT_PROP_ITEMS_N IN (324, 325, 511)
	AND V.TD = DATE '3000-01-01'
	AND DBMS_LOB.INSTR(KK_CONVERT.BLOB_TO_CLOB(V.VAL_BLOB), :B1 ) > 0
	AND A.R_OBJ_N(+) = V.N
	AND A.TABLENAME(+) = 'CLIENT_PROP_VALUES'
	AND U.N(+) = A.PRM_USERS_N
	AND U.TD(+) = DATE '3000-01-01'
	AND INDET.N(+) = V.CLIENT_N
	AND INDET.TD(+) = DATE '3000-01-01'
	AND INDOC.N(+) = INDET.SQ_ST_DOC_IN_N
	AND INDOC.TD(+) = DATE '3000-01-01'
	AND N.N = 
	CASE
		WHEN V.CLIENT_PROP_ITEMS_N = 324 THEN INDET.NOMENKLATURA_N
		WHEN V.CLIENT_PROP_ITEMS_N = 325 THEN PD.NOMENKLATURA_N
		WHEN V.CLIENT_PROP_ITEMS_N = 511 THEN J.NOMENKLATURA_N
	END
	AND N.TD = DATE '3000-01-01'
	AND PD.N(+) = V.CLIENT_N
	AND PD.TD(+) = DATE '3000-01-01'
	AND P.N(+) = PD.ST_DOC_PACK_N
	AND P.TD(+) = DATE '3000-01-01'
	AND J.N(+) = V.CLIENT_N
	AND O.N(+) = J.BASE_DOC_N
	AND O.TD(+) = DATE '3000-01-01'
ORDER BY 
	FD

План:

| Id  | Operation                            | Name                  | E-Rows | A-Rows | A-Time       | Buffers | Reads |
|-----|--------------------------------------|-----------------------|--------|--------|--------------|---------|-------|
| 0   | SELECT STATEMENT                     |                       |        | 1      | 00:01:00.41  | 12M     | 327K  |
| 1   | SORT AGGREGATE                       |                       | 1      | 1      | 00:00:00.01  | 810     | 0     |
|* 2  | COLLECTION ITERATOR PICKLER FETCH    | CLOB_TO_TABLE         | 408    | 1      | 00:00:00.01  | 810     | 0     |
| 3   | SORT ORDER BY                        |                       | 33169  | 1      | 00:01:00.41  | 12M     | 327K  |
| 4   | COUNT                                |                       |        | 1      | 00:01:00.41  | 12M     | 327K  |
|* 5  | HASH JOIN                            |                       | 33169  | 1      | 00:01:00.41  | 12M     | 327K  |
|* 6  | HASH JOIN RIGHT OUTER                |                       | 33234  | 1      | 00:00:58.91  | 12M     | 327K  |
|* 7  | TABLE ACCESS FULL                    | PRM_USERS             | 511    | 477    | 00:00:00.01  | 15      | 0     |
|* 8  | HASH JOIN OUTER                      |                       | 33234  | 1      | 00:00:58.91  | 12M     | 327K  |
|* 9  | HASH JOIN OUTER                      |                       | 33234  | 1      | 00:00:58.34  | 12M     | 327K  |
| 10  | NESTED LOOPS OUTER                   |                       | 33074  | 1      | 00:00:39.97  | 12M     | 158K  |
|* 11 | HASH JOIN RIGHT OUTER                |                       | 33074  | 1      | 00:00:39.97  | 12M     | 158K  |
|* 12 | TABLE ACCESS FULL                    | ST_DOC_IN             | 64184  | 64586  | 00:00:00.03  | 2013    | 0     |
|* 13 | HASH JOIN OUTER                      |                       | 33074  | 1      | 00:00:39.89  | 12M     | 158K  |
|* 14 | HASH JOIN OUTER                      |                       | 33074  | 1      | 00:00:39.42  | 12M     | 158K  |
|* 15 | HASH JOIN OUTER                      |                       | 33074  | 1      | 00:00:35.78  | 12M     | 158K  |
| 16  | PARTITION LIST ALL                   |                       | 33023  | 1      | 00:00:25.90  | 12M     | 122K  |
|* 17 | TABLE ACCESS FULL                    | CLIENT_PROP_VALUES    | 33023  | 1      | 00:00:25.90  | 12M     | 122K  |
|* 18 | TABLE ACCESS FULL                    | ST_DOC_IN_DET         | 1804K  | 1797K  | 00:00:08.35  | 36243   | 36240 |
|* 19 | TABLE ACCESS FULL                    | ST_DOC_PACK_DET       | 3201K  | 3211K  | 00:00:01.05  | 36003   | 0     |
|* 20 | TABLE ACCESS FULL                    | ST_DOC_PACK           | 416K   | 421K   | 00:00:00.14  | 3652    | 0     |
| 21  | PARTITION HASH ITERATOR              |                       | 1      | 0      | 00:00:00.01  | 4       | 0     |
| 22  | TABLE ACCESS BY LOCAL INDEX ROWID    | ACTIONS               | 1      | 0      | 00:00:00.01  | 4       | 0     |
|* 23 | INDEX RANGE SCAN                     | ACTIONS_TBL_OBJ       | 1      | 0      | 00:00:00.01  | 4       | 0     |
| 24  | PARTITION LIST ALL                   |                       | 10M    | 10M    | 00:00:09.50  | 168K    | 168K  |
| 25  | TABLE ACCESS FULL                    | ST_BAR_JOBS           | 10M    | 10M    | 00:00:04.34  | 168K    | 168K  |
|* 26 | TABLE ACCESS FULL                    | ST_DOC_OUT            | 441K   | 442K   | 00:00:00.22  | 17374   | 0     |
|* 27 | TABLE ACCESS FULL                    | NOMENKLATURA          | 1215K  | 1220K  | 00:00:00.49  | 30513   | 0     |

Рекомендации qwen3.8-max*** (далее текст написан ИИ)

Запрос не оптимизирован: стоимость плана высокая, время оценивается в минуты, в плане много полно сканирующих операций и дорогих операций над LOB.

Основные узкие места:

1. Полный скан CLIENT_PROP_VALUES

В плане: PARTITION LIST ALL + TABLE ACCESS FULL CLIENT_PROP_VALUES.

Таблица большая: ~3.5 млн строк, 885 МБ.

Фильтры есть:

CLIENT_PROP_ITEMS_N IN (324, 325, 511)

TD = DATE ‘3000-01-01’

Существует индекс CLIENT_PROP_VALUES_I9 (CLIENT_PROP_ITEMS_N, TD, FD), но он не используется.

Причина, вероятно:

нужен доступ к VAL_BLOB;

функция KK_CONVERT.BLOB_TO_CLOB(V.VAL_BLOB) в WHERE;

оптимизатор считает полный скан дешевле;

2. Очень дорогая работа с BLOB/CLOB

В WHERE выполняется:

DBMS_LOB.INSTR(KK_CONVERT.BLOB_TO_CLOB(V.VAL_BLOB), :B1) > 0

В скалярном подзапросе снова выполняется:

KK_ASB_ST_DOC.CLOB_TO_TABLE(KK_CONVERT.BLOB_TO_CLOB(V.VAL_BLOB))

Фактически конвертация и разбор LOB выполняются многократно.

Поиск по содержимому LOB не может использовать обычный B-tree-индекс.

3. Полные сканы больших таблиц

ST_BAR_JOBS: ~10.5 млн строк, полный скан всех партиций.

ST_DOC_PACK_DET: ~3.2 млн строк.

ST_DOC_IN_DET: ~2 млн строк.

NOMENKLATURA: ~1.2 млн строк.

ST_DOC_OUT: ~450 тыс. строк.

Для большинства из них есть подходящие индексы по N, TD, но план использует HASH JOIN и полные сканы.

4. Сложные CASE в условиях соединения

Предикат:

N.N = CASE WHEN V.CLIENT_PROP_ITEMS_N = 324 THEN INDET.NOMENKLATURA_N …

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

5. Все таблицы соединяются сразу для всех трёх типов

Для CLIENT_PROP_ITEMS_N = 324 реально нужны INDET, INDOC.

Для 325 нужны PD, P.

Для 511 нужны J, O.

Но в запросе все таблицы присутствуют одновременно.

Это может приводить к лишним соединениям и даже к нежелательному умножению строк, если по PD, J есть несколько совпадений.

Рекомендации:

Рекомендация 1: Переписывание запроса - разделение на три логические ветки через UNION ALL

Рекомендация 2: Индекс для CLIENT_PROP_VALUES под фильтры запроса

Тип оптимизации: индекс.

Действие:

Создать локальный индекс, который покрывает основные предикаты:

CLIENT_PROP_ITEMS_N TD FD CLIENT_N

Существующий индекс:

CLIENT_PROP_VALUES_I9 (CLIENT_PROP_ITEMS_N, TD, FD)

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

После модификации, предложенной qwen3.8-max, запрос стал выглядеть так:

with v as
(
	select
		n, fd, client_n, client_prop_items_n, val_clob
	from
	(
		select
			n, fd, client_n, client_prop_items_n,
			kk_convert.blob_to_clob(val_blob) AS val_clob
		from
			client_prop_values
		where
			length(:B1) > 7
			and client_prop_items_n IN (324, 325, 511)
			and td = date '3000-01-01'
	)
	where
		dbms_lob.instr(val_clob, :B1) > 0
),
base as (
	-- 324: Приход
	select
		v.fd,
		u.name as person_name,
		indoc.contragent_n,
		n.code as nom_code,
		'Приход' as oper_name,
		indoc.n as doc_n,
		indoc.num_invoice as doc,
		v.val_clob
	from
		v
	left join
		actions a
	on
		a.r_obj_n = v.n
		and a.tablename = 'CLIENT_PROP_VALUES'
	left join
		prm_users u
	on
		u.n = a.prm_users_n
		and u.td = date '3000-01-01'
	join
		st_doc_in_det indet
	on
		indet.n = v.client_n
		and indet.td = date '3000-01-01'
	join
		st_doc_in indoc
	on
		indoc.n = indet.sq_st_doc_in_n
		and indoc.td = date '3000-01-01'
	join
		nomenklatura n
	on
		n.n = indet.nomenklatura_n
		and n.td = date '3000-01-01'
	where
		v.client_prop_items_n = 324
	union all
	-- 325: упаковка
	select
		v.fd,
		u.name as person_name,
		p.contragent_n,
		n.code as nom_code,
		'Упаковка' as oper_name,
		p.n as doc_n,
		p.doc_num as doc,
		v.val_clob
	from
		v
	left join
		actions a
	on
		a.r_obj_n = v.n
		and a.tablename = 'CLIENT_PROP_VALUES'
	left join
		prm_users u
	on
		u.n = a.prm_users_n
		and u.td = date '3000-01-01'
	join
		st_doc_pack_det pd
	on
		pd.n = v.client_n
		and pd.td = date '3000-01-01'
	join
		st_doc_pack p
	on
		p.n = pd.st_doc_pack_n
		and p.td = date '3000-01-01'
	join
		nomenklatura n
	on
		n.n = pd.nomenklatura_n
		and n.td = date '3000-01-01'
	where
		v.client_prop_items_n = 325
	union all
	-- 511: подборка
	select
		v.fd,
		u.name as person_name,
		j.contragent_n,
		n.code as nom_code,
		'Подборка' as oper_name,
		o.n as doc_n,
		o.doc as doc,
		v.val_clob
	from
		v
	left join
		actions a
	on
		a.r_obj_n = v.n
		and a.tablename = 'CLIENT_PROP_VALUES'
	left join
		prm_users u
	on
		u.n = a.prm_users_n
		and u.td = date '3000-01-01'
	join
		st_bar_jobs j
	on
		j.n = v.client_n
	join
		st_doc_out o
	on
		o.n = j.base_doc_n
		and o.td = date '3000-01-01'
	join
		nomenklatura n
	on
		n.n = j.nomenklatura_n
		and n.td = date '3000-01-01'
	where
		v.client_prop_items_n = 511
)
select
	ROWNUM AS RN,
	(
	select
		STRAGG(VALUE(t))
	from
		TABLE(KK_ASB_ST_DOC.CLOB_TO_TABLE(b.val_clob)) t
	WHERE
		dbms_lob.instr(VALUE(t), :B1) > 0
	) AS sn,
	b.fd,
	b.person_name,
	b.contragent_n,
	b.nom_code,
	b.oper_name,
	b.doc_n,
	b.doc
from
	base b
order by
	b.fd;

Конец блока, сформированного ИИ

План:

| Id  | Operation                            | Name                       | E-Rows | A-Rows | A-Time       | Buffers | Reads |
|-----|--------------------------------------|----------------------------|--------|--------|--------------|---------|-------|
| 0   | SELECT STATEMENT                     |                            |        | 1      | 00:00:29.88  | 12M     | 122K  |
| 1   | SORT AGGREGATE                       |                            | 1      | 1      | 00:00:00.01  | 899     | 0     |
|* 2  | COLLECTION ITERATOR PICKLER FETCH    | CLOB_TO_TABLE              | 408    | 1      | 00:00:00.01  | 899     | 0     |
| 3   | SORT ORDER BY                        |                            | 25010  | 1      | 00:00:29.88  | 12M     | 122K  |
| 4   | COUNT                                |                            |        | 1      | 00:00:29.88  | 12M     | 122K  |
| 5   | VIEW                                 |                            | 25010  | 1      | 00:00:29.88  | 12M     | 122K  |
| 6   | UNION-ALL                            |                            |        | 1      | 00:00:29.88  | 12M     | 122K  |
|* 7  | HASH JOIN RIGHT OUTER                |                            | 3475   | 0      | 00:00:01.18  | 526K    | 14932 |
|* 8  | TABLE ACCESS FULL                    | PRM_USERS                  | 511    | 477    | 00:00:00.01  | 15      | 0     |
| 9   | NESTED LOOPS OUTER                   |                            | 3475   | 0      | 00:00:01.18  | 526K    | 14932 |
|* 10 | HASH JOIN                            |                            | 3240   | 0      | 00:00:01.18  | 526K    | 14932 |
|* 11 | HASH JOIN                            |                            | 3240   | 0      | 00:00:01.18  | 526K    | 14932 |
|* 12 | HASH JOIN                            |                            | 3693   | 0      | 00:00:01.18  | 526K    | 14932 |
|* 13 | TABLE ACCESS BY GLOBAL INDEX ROWID   | CLIENT_PROP_VALUES         | 3687   | 0      | 00:00:01.17  | 526K    | 14932 |
|* 14 | INDEX RANGE SCAN                     | CLIENT_PROP_VALUES_I3      | 74634  | 26656  | 00:00:00.02  | 111     | 0     |
|* 15 | TABLE ACCESS FULL                    | ST_DOC_IN_DET              | 1804K  | 0      | 00:00:00.01  | 0       | 0     |
|* 16 | INDEX FAST FULL SCAN                 | ST_DOC_IN_N_TD_COVER       | 64184  | 0      | 00:00:00.01  | 0       | 0     |
|* 17 | INDEX FAST FULL SCAN                 | NOMENKLATURA_N_TD_CODE     | 1215K  | 0      | 00:00:00.01  | 0       | 0     |
| 18  | PARTITION HASH ITERATOR              |                            | 1      | 0      | 00:00:00.01  | 0       | 0     |
|* 19 | INDEX RANGE SCAN                     | ACTIONS_TBL_OBJ_PRMUSR     | 1      | 0      | 00:00:00.01  | 0       | 0     |
|* 20 | HASH JOIN RIGHT OUTER                |                            | 19636  | 1      | 00:00:27.41  | 11M     | 107K  |
|* 21 | TABLE ACCESS FULL                    | PRM_USERS                  | 511    | 477    | 00:00:00.01  | 15      | 0     |
| 22  | NESTED LOOPS OUTER                   |                            | 19636  | 1      | 00:00:27.41  | 11M     | 107K  |
|* 23 | HASH JOIN                            |                            | 18310  | 1      | 00:00:27.41  | 11M     | 107K  |
|* 24 | HASH JOIN                            |                            | 18310  | 1      | 00:00:26.95  | 11M     | 107K  |
|* 25 | HASH JOIN                            |                            | 18310  | 1      | 00:00:25.67  | 11M     | 107K  |
| 26  | PARTITION LIST ALL                   |                            | 18310  | 1      | 00:00:22.26  | 11M     | 107K  |
|* 27 | TABLE ACCESS FULL                    | CLIENT_PROP_VALUES         | 18310  | 1      | 00:00:22.26  | 11M     | 107K  |
|* 28 | INDEX FAST FULL SCAN                 | ST_DOC_PACK_DET_N_TD_COVER | 3201K  | 3211K  | 00:00:00.99  | 15953   | 433   |
|* 29 | INDEX FAST FULL SCAN                 | NOMENKLATURA_N_TD_CODE     | 1215K  | 1220K  | 00:00:00.37  | 5798    | 0     |
|* 30 | TABLE ACCESS FULL                    | ST_DOC_PACK                | 416K   | 421K   | 00:00:00.14  | 3652    | 0     |
| 31  | PARTITION HASH ITERATOR              |                            | 1      | 0      | 00:00:00.01  | 4       | 0     |
|* 32 | INDEX RANGE SCAN                     | ACTIONS_TBL_OBJ_PRMUSR     | 1      | 0      | 00:00:00.01  | 4       | 0     |
|* 33 | HASH JOIN RIGHT OUTER                |                            | 1899   | 0      | 00:00:01.29  | 611K    | 21    |
|* 34 | TABLE ACCESS FULL                    | PRM_USERS                  | 511    | 477    | 00:00:00.01  | 15      | 0     |
| 35  | NESTED LOOPS OUTER                   |                            | 1899   | 0      | 00:00:01.29  | 610K    | 21    |
|* 36 | HASH JOIN                            |                            | 1771   | 0      | 00:00:01.29  | 610K    | 21    |
|* 37 | HASH JOIN                            |                            | 1822   | 0      | 00:00:01.29  | 610K    | 21    |
| 38  | NESTED LOOPS                         |                            | 3985   | 0      | 00:00:01.29  | 610K    | 21    |
| 39  | NESTED LOOPS                         |                            | 3985   | 0      | 00:00:01.29  | 610K    | 21    |
|* 40 | TABLE ACCESS BY GLOBAL INDEX ROWID   | CLIENT_PROP_VALUES         | 3966   | 0      | 00:00:01.29  | 610K    | 21    |
|* 41 | INDEX RANGE SCAN                     | CLIENT_PROP_VALUES_I3      | 80277  | 37974  | 00:00:00.03  | 160     | 1     |
|* 42 | INDEX RANGE SCAN                     | ST_BAR_JOBS_N_TD_FD        | 1      | 0      | 00:00:00.01  | 0       | 0     |
| 43  | TABLE ACCESS BY GLOBAL INDEX ROWID   | ST_BAR_JOBS                | 1      | 0      | 00:00:00.01  | 0       | 0     |
|* 44 | TABLE ACCESS FULL                    | ST_DOC_OUT                 | 441K   | 0      | 00:00:00.01  | 0       | 0     |
|* 45 | INDEX FAST FULL SCAN                 | NOMENKLATURA_N_TD_CODE     | 1215K  | 0      | 00:00:00.01  | 0       | 0     |
| 46  | PARTITION HASH ITERATOR              |                            | 1      | 0      | 00:00:00.01  | 0       | 0     |
|* 47 | INDEX RANGE SCAN                     | ACTIONS_TBL_OBJ_PRMUSR     | 1      | 0      | 00:00:00.01  | 0       | 0     |

Увеличение быстродействия: в 2 раза, количество Reads уменьшилось почти в 3 раза.

Ушли полные сканирования (TABLE ACCESS FULL) таблиц ST_DOC_PACK_DET, ST_BAR_JOBS, хотя остались для ST_DOC_IN_DET, ST_DOC_PACK и CLIENT_PROP_VALUES.

Присутствует тройное чтение CLIENT_PROP_VALUES: CTE V используется трижды внутри UNION ALL, но оптимизатор не материализует его автоматически (можно добавить хинт /*+ MATERIALIZE */). В плане это выражается тремя независимыми доступами к CLIENT_PROP_VALUES. Очевидно, что данный запрос можно продолжить оптимизировать, но уже без использования LLM.

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

V. Опыт эксплуатации.

В ходе опытной эксплуатации было замечено, что локальные модели, которые я смог установить на свой ноутбук с 16 ГБ памяти: qwen3:14b и gemma4:26b**** - справляются только с простыми задачами.

К примеру:

============================================================
Объект: BI_SHIPMENTS_RESERVE (TABLE)
============================================================
Размер: 3054 МБ, TS: USERS
Столбцы:
ДАТАЗАГРУЗКИ DATE NULL NDV=611
ДАТА DATE NULL NDV=625
СЧЁТФАКТУРА CHAR NULL NDV=135872
НОМЕРАКТА CHAR NULL NDV=75816
КОДКОНТРАГЕНТА CHAR NULL NDV=92
КОДНОМЕНКЛАТУРЫ CHAR NULL NDV=42016
СКЛАД CHAR NULL NDV=14
ПЛАН FLOAT NULL NDV=1951
ЗАРЕЗЕРВИРОВАНО FLOAT NULL NDV=2008
ПОДОБРАНО FLOAT NULL NDV=1994
ОТГРУЖЕНО FLOAT NULL NDV=1994

Рекомендации gemma4:26b (далее текст написан ИИ)

Анализ текущего состояния

Истинная причина низкой производительности (20 минут на 1.4 млн строк):

Катастрофическая ошибка в проектировании схемы данных - использование типа CHAR вместо VARCHAR2 для строковых полей.

Тип CHAR всегда дополняет значение пробелами до заявленной длины.

  • СКЛАД CHAR(1000) занимает 1000 байт на каждую строку, даже если название склада - 5 символов.

  • КОДНОМЕНКЛАТУРЫ CHAR(200), СЧЁТФАКТУРА CHAR(100) и т.д.

В результате средняя длина строки (avg_row_len) раздута до 1483 байт. Таблица весом 3 ГБ на диске содержит по факту лишь несколько сотен мегабайт полезных данных. Оптимизатор вынужден читать 390 041 блок (3 ГБ) с диска в память, что создаёт колоссальную нагрузку на подсистему ввода-вывода (I/O) и потребляет огромный объем PGA/SGA.

Конец блока, сформированного ИИ

Для более сложных запросов локальные модели не справлялись и галлюцинировали - предлагали использование таблиц, которых нет в базе данных.

Облачные модели Alibaba Cloud - qwen3, хорошо анализировали сложные SQL-запросы и предлагали полезные советы. Ценно и то, что модели предлагали проанализировать использование индексов, близких к созданным, и по результату оставить или удалить их.

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

Доступа к другим облачным моделям у меня не было - поэтому, если вам будет интересна эта программа и вы подключите другие облачные LLM - будет замечательно, если поделитесь опытом в комментариях.

VI. Безопасность и приватность.

Тема, которую хочется обойти стороной, но нельзя. Инструмент, который читает планы, структуру таблиц, статистику и тексты SQL, а потом отправляет всё это в LLM - это по определению риск утечки. Ниже - честно о том, с чем вы можете столкнутся, если будете использовать Optimus Oracle (или любой похожий инструмент) на реальной базе.

Что именно покидает базу данных

Даже если программа работает только с SELECT-запросами к системным представлениям, объём предоставляемой информации впечатляет:

  • Тексты SQL-запросов. Со всеми именами таблиц, колонок, схем, константами в предикатах. Иногда - с литералами, которые в проде не заменены на bind-переменные (например, номер контракта, ИНН, e-mail).

  • Планы выполнения. Из них видно структуру схемы: какие таблицы связаны, какие индексы есть, как партиционированы, сколько примерно строк.

  • Статистика. NUM_DISTINCT, AVG_ROW_LEN, гистограммы - по ним можно восстановить распределение значений в колонках.

  • Метаданные объектов. DDL-подобное описание таблиц, индексов, ограничений.

  • Фрагменты данных. В скалярных подзапросах и CASE-выражениях иногда встречаются литералы (‘Приход’, ‘Упаковка’, ‘Подборка’ как в нашем примере) - это уже бизнес-логика.

Если вы подключаете облачную LLM - всё это уходит на серверы провайдера (Google Cloud, Alibaba Cloud и т.д.), где хранится в логах в соответствии с их политикой. Даже если провайдер обещает «не использовать данные для обучения», сам факт передачи по сети - это уже канал утечки информации, который нужно осознанно закрывать.

Облако vs локальные модели

Простой критерий выбора:

Критерий

Локальные (Ollama)

Облачные (OpenAI, Alibaba)

Данные покидают периметр

Нет

Да

Качество ответов на сложных запросах

Среднее

Высокое

Стоимость

Железо + электричество

Оплата за токены

Скорость

Зависит от GPU

Обычно выше

Юридические риски

Минимальные

Зависит от политики

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

Галлюцинации - это не баг, а свойство

LLM может уверенно предложить:

  • индекс на таблицу, которой нет в базе;

  • переписать запрос с использованием колонки, которой не существует.

Правило: любая рекомендация LLM - это черновик для разработчика, а не готовое решение. В самом инструменте это заложено: он ничего не выполняет сам, только предлагает. Финальное решение - всегда за человеком.

VII. Заключение:

Кто-нибудь спросит: «Зачем ты всё это написал? Опять статья про ИИ».

- Что вы всё пишете?

- Описываю предметы, ощущения. Людей. Я теперь каждый день пишу, надеясь спасти их от забвения.

- Мир Божий слишком велик, чтобы рассчитывать здесь на успех.

- Знаете, если каждый опишет свою, пусть небольшую, частицу этого мира… Хотя почему, собственно, небольшую? Всегда ведь найдётся тот, чей обзор достаточно широк*****.

Примечания:

* М. Э. Шеффер и Э. Бэрроуз «Клуб любителей книг и пирогов из картофельных очистков».

** Томас Кайт, Дарл Кун «Oracle для профессионалов» 3-е издание.

*** https://docs.modelstudio.console.alibabacloud.com/en/model-studio/qwen3-8-max?ref=bobzhu.tech

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.