DB developers channel
804 subscribers
2 photos
46 files
102 links
💡 Канал для разработчиков баз данных: Oracle, PostgreSQL
📌 Интересные задачи, фрагменты кода, лучшие практики, архитектура, оптимизация
🔄 Присоединяйся к сообществу — развивайся вместе с нами!
#SQL #Oracle #PostgreSQL #PL/SQL #PL/pgSQL #DB
Download Telegram
🎵 «Не дорого ценю я громкие слова,
От коих не одна кружится голова.
Я не ропщу о том, что отказали боги
Мне в сладкой участи оспаривать налоги» — Александр Пушкин

📌 Разбор задачи: Проверка на сбалансированные скобки процедурными методами

🧩 Условие:
Нужно проверить, сбалансированы ли скобки в строке.
Скобки считаются сбалансированными, если:
каждой открывающей соответствует закрывающая;
порядок правильный (( { [ ] } )).

⚙️ Алгоритм (кратко):
1️⃣ Создаём стек для хранения открывающихся скобок.
2️⃣ Идём по строке символ за символом.
3️⃣ Если встречаем '(', '{', '[' → кладём в стек.
4️⃣ Если встречаем ')', '}', ']':
• если стек пустой → ошибка (несбалансировано),
• иначе сверяем, что верх стека — соответствующая открывающая скобка.
Если совпадает → убираем её из стека.
5️⃣ После обхода строки стек должен быть пуст.
6️⃣ Если пуст — всё сбалансировано , иначе — нет .

🔢 Пример:
Строка: (124(){}[[] {123()}  ])
Результат: BALANCED

`SQL
DECLARE
str VARCHAR2(4000) := ' (124(){}[[] {123()} ]) ';

TYPE stack_tab IS TABLE OF VARCHAR2 (1);
stack stack_tab := stack_tab();

indexChar VARCHAR2 (1);
isBalanced VARCHAR2 (1) := 'Y';
BEGIN
FOR index# IN 1..LENGTH(str)
LOOP
indexChar := SUBSTR (str, index#, 1);

IF indexChar NOT IN ('(', '{', '[', ')', '}', ']') THEN
CONTINUE;
END IF;

IF indexChar IN ('(', '{', '[') THEN
stack.extend;
stack(stack.COUNT) := indexChar;
CONTINUE;
END IF;

IF indexChar IN (')', '}', ']') AND stack IS EMPTY THEN
isBalanced := 'N';
EXIT;
END IF;

IF indexChar = ')' THEN
IF stack (stack.COUNT) = '(' THEN
stack.delete(stack.COUNT);
CONTINUE;
ELSE
isBalanced := 'N';
EXIT;
END IF;
END IF;

IF indexChar = '}' THEN
IF stack (stack.COUNT) = '{' THEN
stack.delete(stack.COUNT);
CONTINUE;
ELSE
isBalanced := 'N';
EXIT;
END IF;
END IF;

IF indexChar = ']' THEN
IF stack (stack.COUNT) = '[' THEN
stack.delete(stack.COUNT);
CONTINUE;
ELSE
isBalanced := 'N';
EXIT;
END IF;
END IF;
END LOOP;

IF stack IS NOT EMPTY THEN
isBalanced := 'N';
END IF;

stack.delete;

IF isBalanced = 'N' THEN
dbms_output.put_line ('UNBALANCED');
ELSE
dbms_output.put_line ('BALANCED');
END IF;
EXCEPTION
WHEN OTHERS THEN
IF stack IS NOT NULL AND stack IS NOT EMPTY THEN
stack.delete;
RAISE;
END IF;
END;
`


👍 Палец вверх — если такие алгоритмы полезны.
👎 Палец вниз — если тема себя изжила.

⚠️ Хотите проверить скрипты, но нет базы под рукой - онлайн-песочница
💎 Поддержка канала⁉️

💬 Эльвира свой вариант выставила. А остальные? Что скажет "купечество"?
#️⃣ #SQL #Oracle #PLSQL
👍9👎2
image.png
1 MB
🎵 «Мои друзья — хоть не в болонии
Зато не тащут из семьи
А гадость пьют — из экономии
Хоть поутру — да на свои!» — Владимир Высоцкий

Поле как битовая маска

Представьте: у вас есть таблица с кучей полей-флагов.

Например:
is_internal
is_main
is_admin
is_resident
и т.д.

Все эти поля принимают только два значения: 0/1, 'Y'/'N' или аналогичные.

Но проблема в том, что таких флагов может быть десятки, и аналитики каждую неделю просят добавлять новые.
А это значит — переписывать код, менять десятки объектов в БД и заново накатывать на прод. 😩

Чтобы избежать этого ада, используют битовые маски.

🛠 Способы реализации битовых масок
1️⃣ Через INTEGER (NUMBER(38,0))

Каждый бит отвечает за конкретный флаг.

bit_mask =>
POWER(2,0) * CASE WHEN is_internal = 'Y' THEN 1 ELSE 0 END +
POWER(2,1) * CASE WHEN is_main = 'Y' THEN 1 ELSE 0 END +
POWER(2,2) * CASE WHEN is_admin = 'Y' THEN 1 ELSE 0 END +
POWER(2,3) * CASE WHEN is_resident = 'Y' THEN 1 ELSE 0 END

Пример:
is_internal = Y, остальные = N
👉 маска = 0001₂ = 1

Если is_internal=Y и is_resident=Y:
👉 маска = 1001₂ = 9

⚠️ Ограничение: максимум 38 бит.

2️⃣ Через RAW
Можно хранить как двоичные данные.
Пример для is_internal=Y и is_resident=Y:

1001₂ = 09 (HEX)

SQL-пример:
SELECT UTL_RAW.CAST_TO_RAW(CHR(BIN_TO_NUM(1,0,0,1)))
FROM dual;

3️⃣ Через VARCHAR2

Экзотичную битовую маску в виде VARCHAR2:
bit_mask = 'YNNY'
Каждый символ обозначает состояние флага.

⚖️ Баланс
Идея простая: вместо десятков двоичных полей мы храним одно поле с маской.

Но универсальность стоит ресурсов:
придётся нагружать CPU для декодирования маски
возможно нужен справочник, какой бит за что отвечает

🔥 Если тема интересна — далее покажу имплементацию.
Наберём хотя бы 20 🤓.

👍 Палец вверх — тема зашла.
👎 Палец вниз — не зашла.

⚠️ Хотите проверить скрипты, но нет базы под рукой - онлайн-песочница
💎 Поддержка канала⁉️

💬 Справедливую и не очень критику туда. 👇
#SQL #Oracle #PLSQL #CodeArchitecture
👍162
🎵 «Деньги бывают — такое бывает…» — Ленинград

📊 Вычисление дней просрочки по клиентам и счетам

Эта задача встречается на собеседованиях и в банковской аналитике.
Количество дней просрочки — ключевой показатель для расчёта резервов.

🔹 Условие задачи

Таблица движения средств по просроченной задолженности:
amount > 0 → долг возник, день считается первым днём просрочки
amount < 0 → долг уменьшается
Когда накопленный долг = 0 → задолженность погашена, первый и последний день включаются

💡 Важно: погашение происходит по принципу FIFO — сначала гасим старые долги, потом новые,
т.е. если клиент вовремя не погасил долг, то с этого дня начинается рассчет количества дней просрочки.
Если клиент вносил деньги для уплаты долга, то банк обязан сначала эти деньги учесть в самых старых просроченных платежах,
а потом, если остается остаток, то пробует погасить более новые долги.

Возможно, у разработчиков, не знакомых с кредитными задачами, возникнет сложность с логикой.
Во-первых, это нормально! Я только к банковским терминам привыкал полгода (План счетов, Дебет, Кредит, Просрочка, Вынос на просрочку, Погашение, Комиссии, Пенни, и прочий ужас).
Во-вторых, никто не обещал легкой жизни.

🔹 Таблица движения средств
CREATE TABLE account_movements (
client_id NUMBER,
account_id NUMBER,
payment_date DATE,
amount NUMBER
);


🔹 Пример данных
-- Клиент 1 — ежемесячные долги с частичными погашениями, полное погашение
INSERT INTO account_movements VALUES (1, 101, DATE '2024-01-01', 100);
INSERT INTO account_movements VALUES (1, 101, DATE '2024-02-01', 100);
INSERT INTO account_movements VALUES (1, 101, DATE '2024-03-01', 100);
INSERT INTO account_movements VALUES (1, 101, DATE '2024-04-01', 100);
INSERT INTO account_movements VALUES (1, 101, DATE '2024-05-01', 100);
INSERT INTO account_movements VALUES (1, 101, DATE '2024-06-01', 100);

INSERT INTO account_movements VALUES (1, 101, DATE '2024-01-15', -99);
INSERT INTO account_movements VALUES (1, 101, DATE '2024-02-15', -99);
INSERT INTO account_movements VALUES (1, 101, DATE '2024-03-15', -99);
INSERT INTO account_movements VALUES (1, 101, DATE '2024-04-15', -99);
INSERT INTO account_movements VALUES (1, 101, DATE '2024-05-15', -99);
INSERT INTO account_movements VALUES (1, 101, DATE '2024-06-15', -99);

INSERT INTO account_movements VALUES (1, 101, DATE '2024-07-01', -6);

-- Клиент 2 — медленные погашения
INSERT INTO account_movements VALUES (2, 201, DATE '2024-01-01', 50);
INSERT INTO account_movements VALUES (2, 201, DATE '2024-02-01', 50);
INSERT INTO account_movements VALUES (2, 201, DATE '2024-03-01', 50);
INSERT INTO account_movements VALUES (2, 201, DATE '2024-04-01', 50);
INSERT INTO account_movements VALUES (2, 201, DATE '2024-05-01', 50);
INSERT INTO account_movements VALUES (2, 201, DATE '2024-06-01', 50);

INSERT INTO account_movements VALUES (2, 201, DATE '2024-02-15', -20);
INSERT INTO account_movements VALUES (2, 201, DATE '2024-03-20', -30);
INSERT INTO account_movements VALUES (2, 201, DATE '2024-05-10', -50);

INSERT INTO account_movements VALUES (2, 201, DATE '2024-08-01', -150);

-- Клиент 3 — не погашения
INSERT INTO account_movements VALUES (3, 301, DATE '2024-01-01', 200);
INSERT INTO account_movements VALUES (3, 301, DATE '2024-02-01', 200);
INSERT INTO account_movements VALUES (3, 301, DATE '2024-03-01', 200);
INSERT INTO account_movements VALUES (3, 301, DATE '2024-04-01', 200);
INSERT INTO account_movements VALUES (3, 301, DATE '2024-05-01', 200);
INSERT INTO account_movements VALUES (3, 301, DATE '2024-06-01', 200);

💡 Ожидаемый результат на 15.05.2024

Клиент Задолженность Дней просрочки Период
1 5 15 2024-05-01 – 2024-05-15
2 150 76 2024-03-01 – 2024-05-15
3 1000 136 2024-01-01 – 2024-05-15

В реальной жизни долги бывают разных видов, но принцип одинаков.

⚠️ Хотите проверить скрипты, но нет базы под рукой - онлайн-песочница
💎 Поддержка канала⁉️

👍 Банковская тема интересна
👎 Такой ужас видеть больше не хочу
#️⃣ #RealInterviewTasks #SQL #Oracle #PLSQL #PostgreSQL
👍5👎1
🎵 «Если друг оказался вдруг
И не друг, и не враг, а — так,
Если сразу не разберёшь,
Плох он или хорош...» — Владимир Высоцкий

Сегодня суббота — разбор задач оставим на будущее.
Продолжаем рубрику «Полезные ресурсы».

🖥️ Тема: Работа с Git и анализ кода

Раньше для работы с Git я использовал Git Extensions
для управления репозиториями, а для поиска и анализа файлов — Total Commander и Notepad++.

Но коллеги помогли (или слегка заставили 😉) перейти на JetBrains IntelliJ IDEA.
Сложная и мощная программа, к ней нужно привыкнуть, но она того стоит!
Особенно мне нравится поиск по ключевым словам и файлам, а также встроенные инструменты для анализа кода.
Вообще, если вы работаете с проектами в Git и большими кодовыми базами — рекомендую попробовать.

💎 Поддержка канала⁉️

👨‍💻 А вы используете Git? Какими инструментами для анализа файлов пользуетесь?
Делитесь в комментариях 👇
#️⃣ #Tools
1👍1🔥1
🎵 «Две звезды - две светлых повести,
В своей любви, как в невесомости.
Два голоса среди молчания,
В небесном храме звезд венчание.» — Алла Пугачева

📌 Разбор задачи: Два указателя — проверить, содержит ли отсортированный массив два числа, сумма которых равна заданному числу target.

Представьте, что у вас есть ряд чисел, и вам нужно найти пару, которая даёт нужную сумму.
Алгоритм (кратко):
– ставит «левый» указатель на начало массива,
– «правый» указатель — в конец,
– двигает их навстречу друг другу в зависимости от суммы текущей пары.

DECLARE
TYPE set_tab IS TABLE OF INTEGER;

"set" set_tab := set_tab (0,1,2,3,4,5,6,7,8,10);
target INTEGER := 15;
currentTarget INTEGER := 0;
leftIndex INTEGER := 1;
rightIndex INTEGER := "set".LAST;
isTargetExist VARCHAR2(1) := 'N';
BEGIN
WHILE leftIndex != rightIndex LOOP
currentTarget := "set"(leftIndex) + "set"(rightIndex);

IF currentTarget = target THEN
isTargetExist := 'Y';
EXIT;
ELSIF currentTarget > target THEN
rightIndex := rightIndex - 1;
ELSE
leftIndex := leftIndex + 1;
END IF;
END LOOP;

IF isTargetExist = 'Y' THEN
DBMS_OUTPUT.put_line('exist');
ELSE
DBMS_OUTPUT.put_line('not exist');
END IF;
END;

💡 Логика проста:
– Если сумма больше нужной — двигаем правый указатель влево;
– Если сумма меньше — двигаем левый указатель вправо;
– Если нашли — выход!

👍 Палец вверх — если нравится.
👎 Палец вниз — если не нравится.

⚠️ Хотите проверять скрипты без базы — используйте онлайн-песочницу.
💎 Поддержка канала⁉️

💬 Есть что добавить? 👇
#️⃣ #SQL #Oracle #PLSQL
👍41👎1
«Вы, конечно, стоите на краю финансовой пропасти? — спросил он Балаганова.
Это вы насчет денег? — сказал Шура. — Денег у меня нет уже целую неделю.
В таком случае вы плохо кончите, молодой, человек, — наставительно сказал Остап.
Финансовая пропасть-самая глубокая из всех пропастей, в нее можно падать всю жизнь.»
— Илья Ильф и Евгений Петров.

📊 Разбор задачи «Вычисление дней просрочки по клиентам и счетам»

"Пара слов без протокола"...
В банках подготовка данных для получения сводной таблицы долгов и погашений занимает приличное время.
Дело в том, что есть разные виды задолженностей и разный приоритет погашений для каждого вида задолженности.
А также есть корректирующие операции, которые проводятся задним числом.
Вообщем, это демо модель и я не претендую, что она оптимальная, так как у неё есть недостатки.
Недостаток в том, что приходится считать FIFO баланс даже тогда, когда уже в этом нет необходимости.
Из-за этого, я видел и процедурную реализацию данной задачи.
Какое решение подойдет именно Вам, нужно тестировать и разбираться.

Теперь разберем решение на SQL.

🔹 Шаг 1. Движение и накопленный FIFO-баланс
Сначала нам нужно посчитать «накопленный долг» на каждый день с учётом FIFO.
Для этого мы суммируем все возникшие долги (amount > 0) по дате, и отдельно все погашения (amount < 0) по счёту:
SELECT am.*,
SUM(CASE WHEN am.amount > 0 THEN am.amount ELSE 0 END)
OVER (PARTITION BY am.client_id, am.account_id
ORDER BY am.payment_date)
+ SUM(CASE WHEN am.amount < 0 THEN am.amount ELSE 0 END)
OVER (PARTITION BY am.client_id, am.account_id) AS fifo_balance
FROM account_movements am
WHERE am.payment_date <= DATE '2024-05-15'


👉 Здесь ключевая идея:
Долги мы считаем, как накопительную сумму.
Погашения долга считаем, как общую сумму по клиенту и по договору (счету)

🔹 Шаг 2. Определяем дату начала просрочки
Теперь для каждого клиента/счёта нужно найти первую дату, когда долг стал больше 0:
MIN(CASE WHEN am1.fifo_balance > 0 THEN am1.payment_date END) AS first_overdue_date

Если долг никогда не возникал → просрочки нет.

🔹 Шаг 3. Рассчитываем количество дней просрочки

Если у клиента есть дата первой просрочки, то количество дней = разница между контрольной датой и этой датой (оба дня включаются):
CASE
WHEN first_overdue_date IS NULL THEN 0
ELSE DATE '2024-05-15' - first_overdue_date + 1
END AS overdue_day_amount


Итого:
SELECT am2.client_id,
am2.account_id,
CASE
WHEN first_overdue_date IS NULL THEN 0
ELSE DATE '2024-05-15' - first_overdue_date + 1
END AS overdue_day_amount
FROM (
SELECT am1.client_id,
am1.account_id,
MIN(CASE WHEN am1.fifo_balance > 0 THEN am1.payment_date END) AS first_overdue_date
FROM (
SELECT am.*,
SUM(CASE WHEN am.amount > 0 THEN am.amount ELSE 0 END)
OVER (PARTITION BY am.client_id, am.account_id
ORDER BY am.payment_date)
+ SUM(CASE WHEN am.amount < 0 THEN am.amount ELSE 0 END)
OVER (PARTITION BY am.client_id, am.account_id) AS fifo_balance
FROM account_movements am
WHERE am.payment_date <= DATE '2024-05-15'
) am1
GROUP BY am1.client_id, am1.account_id
) am2;

💡 Что получилось
Клиент Количество дней просрочки
1 15
2 76
3 136

Есть еще интересней задачка на эту тему - поиск максимального количества дней просрочки по договору.
Задача не про заданную дату, а ретроспективно на всё время жизни договора.
Клиент мог выходить на просрочку несколько раз и в данный момент её не иметь.

👍 Палец вверх — банковская тема интересна.
👎 Палец вниз — Какая муть! Мне это категорически не интересно.

⚠️ Хотите проверить скрипты, но нет базы под рукой - онлайн-песочница
💎 Поддержка канала⁉️

💬 Хотите что-то добавить. 👇
#️⃣ #RealInterviewTasks #SQL #Oracle #PLSQL #PostgreSQL
👍12👎1
🎵 «Пусть все будет так, как ты захочешь.
Пусть твои глаза, как пpежде, гоpят.
Я с тобой опять сегодня этой ночью.
Hу а впpочем, следующей ночью,
Если захочешь, я опять у тебя.» — Чайф.

📌 Случай из практики

Допустим, у нас есть таблица:
CREATE TABLE something (
id NUMBER,
col1 VARCHAR2(20),
col2 VARCHAR2(10),
col3 VARCHAR2(500),
created DATE,
created_by NUMBER,
updated DATE,
updated_by NUMBER
);

🔹 Таблица небольшая, но часто читаемая. Записи в ней редактируются, но не слишком активно.

Требование: изменить поле col1 → нужно перевести его в тип NUMBER и пересчитать значения.
Например:
CASE 
WHEN col1 = 'value1' THEN 1
WHEN col1 = 'value2' THEN 2
ELSE 3
END

Казалось бы, задача простая:

1. Добавляем новое поле col1_new.
2. Заполняем его как нужно.
3. Переименовываем старое поле в col1_old, а новое — в col1.
4. Если всё прошло успешно — col1_old можно убрать или пометить как неиспользуемое.

Но есть дополнительное условие:
Новое поле не должно сместиться в конец списка, а должно остаться на своём месте — сразу после id и перед col2.

🤔 Как бы вы справились с такой задачей?

⚠️ Хотите проверить скрипты, но нет базы под рукой - онлайн-песочница
💎 Поддержка канала⁉️

💬 Ваши мысли и Ваши решения туда. 👇
#️⃣ #Cases #SQL #Oracle #PLSQL #PostgreSQL #PLpgSQL
👍2👎1
DB developers channel
🎵 «Пусть все будет так, как ты захочешь. Пусть твои глаза, как пpежде, гоpят. Я с тобой опять сегодня этой ночью. Hу а впpочем, следующей ночью, Если захочешь, я опять у тебя.» — Чайф. 📌 Случай из практики Допустим, у нас есть таблица: CREATE TABLE something…
🎵 «Дорога, а в дороге МАЗ
Который по уши увяз
В кабине тьма, напарник третий час молчит
Хоть бы кричал, аж зло берёт
Назад пятьсот, вперёд пятьсот
А он зубами танец с саблями стучит.» — Владимир Высоцкий

🛠 Как аккуратно поменять тип колонки в Oracle и сохранить порядок полей

Напомню первоначальную постановку:
есть таблица something (небольшая, но часто читаемая), где поле col1 хранится как VARCHAR2(20), но его нужно привести к типу NUMBER и при этом пересчитать значения по правилу:

CASE 
WHEN col1 = 'value1' THEN 1
WHEN col1 = 'value2' THEN 2
ELSE 3
END

На первый взгляд — простая операция: добавил новую колонку, заполнил, потом переименовал. Но тут есть важное условие:
👉 новая колонка должна остаться на своём месте — сразу после id, а не уехать в конец таблицы.

В Oracle нет встроенного механизма «переместить колонку», поэтому решение чуть хитрее.

🔑 Подход
Ставим таблицу в READ ONLY, чтобы на момент миграции гарантировать отсуствие изменений на источнике.
Переименовываем индексы и констрейны, триггеры на старой таблице.
Создаём новую таблицу с нужной структурой — в ней уже определяем col1 как NUMBER.
Переносим данные из старой таблицы в новую с пересчётом поля col1:
Восстанавливаем объекты на новой таблице (индексы, констрейны, триггеры).
Переименовываем таблицы: старая уходит в архив (something_old), новая становится боевой (something).

📋 Полный скрипт
-- lock the table to prevent modifications
ALTER TABLE something READ ONLY;

-- rename all dependent objects on the table, if any (indexes, constraints, triggers)

-- create a new table (can be done via CTAS if there are no virtual columns)
CREATE TABLE something_new (
id NUMBER,
col1 NUMBER,
col2 VARCHAR2(10),
col3 VARCHAR2(500),
created DATE,
created_by NUMBER,
updated DATE,
updated_by NUMBER
);

-- populate the new table with data from the old one
INSERT INTO something_new (id, col1, col2, col3, created, created_by, updated, updated_by)
SELECT
s.id,
CASE
WHEN s.col1 = 'value1' THEN 1
WHEN s.col1 = 'value2' THEN 2
ELSE 3
END AS col1,
s.col2,
s.col3,
s.created,
s.created_by,
s.updated,
s.updated_by
FROM something s;

-- recreate dependent objects on new table (indexes, constraints, triggers, grants)
-- drop dependent objects on the old table

-- rename the tables
ALTER TABLE something RENAME TO something_old;
ALTER TABLE something_new RENAME TO something;

-- keep something_old for rollback scripts or to preserve original data
-- later it can be safely dropped if no longer needed

🧩 Нюансы
Таблица доступна для чтения, почти всё время, за исключением времени переименования таблиц, но это быстрая операция.
Такой способ гарантирует правильный порядок колонок.
Старую таблицу (something_old) лучше оставить как «бэкап» на время. Если всё прошло успешно — можно дропнуть.

Конечно, такую задачу можно сделать через доп. поле и модификацию текущего поля.
Но таблица будет не доступна больше времени и залочить таблицу не получится.
И тут надо выбирать метод исходя из Ваших обстоятельств.

⚠️ Хотите проверить скрипты, но нет базы под рукой - онлайн-песочница
💎 Поддержка канала⁉️

💬 Если есть другое решение задачи. Поделитесь! 👇
#️⃣ #Cases #SQL #Oracle #PLSQL #PostgreSQL #PLpgSQL
👍5👎1
🎵 «Ну, и меня, конечно, Зин
Всё время тянет в магазин
А там — друзья, я ж, Зин
Не пью один!» - Владимир Высоцкий

📌 Разбор задачи: Удаление N-го узла с конца связного списка
Я продолжаю разбирать задачи и решаю их исключительно процедурными методами.

Представьте себе очередь в магазин.
Все стоят один за другим, и каждый знает только, кто следующий.
И вот приходит администратор и говорит: «Уберите шестого с конца!»

Что делает скрипт?
Сначала пересчитывает всю очередь — сколько людей стоит.
Определяет, кто находится перед тем самым шестым с конца.
Просит этого человека «пропустить следующего» и смотреть уже на того, кто идёт после удаляемого.
А сам шестой — тихо уходит из очереди.
Вот так список сокращается.

CREATE OR REPLACE TYPE node IS OBJECT (VALUE NUMBER, NEXT REF node);
/
CREATE TABLE nodes OF node;
/

DECLARE
stack REF node;

PROCEDURE init_stack (stack OUT REF node, amount IN INTEGER) IS
current1 REF node;
current2 REF node;
BEGIN
FOR i IN 1 .. amount LOOP
IF current1 IS NULL THEN
INSERT INTO nodes n
VALUES (node ("VALUE" => i, NEXT => NULL))
RETURNING REF (n)
INTO current1;

stack := current1;
ELSE
INSERT INTO nodes n
VALUES (node ("VALUE" => i, NEXT => NULL))
RETURNING REF (n)
INTO current2;

UPDATE nodes n
SET n.NEXT = current2
WHERE REF (n) = current1;

current1 := current2;
current2 := NULL;
END IF;
END LOOP;
END init_stack;

PROCEDURE print_stack (stack IN REF node) IS
current REF node;
currentNode node;
i INTEGER := 1;
BEGIN
current := stack;

IF current IS NULL THEN
RETURN;
END IF;

WHILE current IS NOT NULL LOOP
SELECT DEREF (current) INTO currentNode FROM DUAL;

DBMS_OUTPUT.put_line (
'['
|| TO_CHAR (i)
|| '] value '
|| TO_CHAR (currentNode."VALUE")
|| ', '
|| CASE WHEN currentNode.NEXT IS NULL THEN 'next is null' ELSE 'next is not null' END);

i := i + 1;
current := currentNode.next;
END LOOP;
END print_stack;

PROCEDURE delete_node_from_end (stack IN REF node, index# IN INTEGER)
IS
current REF node;
currentNode node;
nodeCounter INTEGER := 0;
properFromBeginIndex# INTEGER := 0;
previousDeletedNode REF node;
deleted REF node;
deletedNode node;
BEGIN
-- считаем количество элементов
current := stack;
WHILE current IS NOT NULL
LOOP
SELECT DEREF (current) INTO currentNode FROM DUAL;
current := currentNode.next;
nodeCounter := nodeCounter + 1;
END LOOP;

properFromBeginIndex# := nodeCounter - index#;

current := stack;
nodeCounter := 0;
previousDeletedNode := NULL;
WHILE current IS NOT NULL AND nodeCounter < properFromBeginIndex#
LOOP
previousDeletedNode := current;
SELECT DEREF (current) INTO currentNode FROM DUAL;
current := currentNode.next;
nodeCounter := nodeCounter + 1;
END LOOP;

SELECT DEREF (previousDeletedNode) INTO currentNode FROM DUAL;
deleted := currentNode.next;

SELECT DEREF (deleted) INTO deletedNode FROM DUAL;

UPDATE nodes n
SET n.next = deletedNode.next
WHERE REF (n) = previousDeletedNode;

DELETE FROM nodes n WHERE REF (n) = deleted;
END delete_node_from_end;
BEGIN
init_stack (stack => stack, amount => 10);

DBMS_OUTPUT.put_line ('stack');
print_stack (stack => stack);

delete_node_from_end (stack => stack, index# => 6);

DBMS_OUTPUT.put_line ('updatedStack');
print_stack (stack => stack);

COMMIT;
END;

👍 Палец вверх — если цикл нужно продолжать.
👎 Палец вниз — если лучше вернуться к DB.

⚠️ Хотите проверить скрипты, но базы нет под рукой — онлайн-песочница вам в помощь.
💎 Поддержка канала⁉️

💬 Обратную связь туда 👇
#️⃣ #SQL #Oracle #PLSQL
👍6👎2
🔆«Тишина должна быть в библиотеке...» — Уральские пельмени

📌 Серия «Оптимизация SQL-запросов»
Этот пост — для тех, у кого возникают проблемы с пониманием плана запроса как такового.

Прежде чем разбирать конкретные планы запросов и их оптимизацию, давайте уясним, что такое план запроса.
Почему СУБД не всегда может гарантировать оптимальный план? И зачем нужны хинты (подсказки)?

📚 До изобретения баз данных человечество уже умело хранить и обрабатывать большие объёмы информации.
Где же? — спросите вы. Конечно же, в библиотеках!
Те, кто разрабатывал модели баз данных, имели опыт работы с библиотеками, и многие термины были заимствованы именно оттуда: индекс, ключевые слова и пр.

Аналогия с библиотекарем
Представим, что мы в самой большой библиотеке мира — Библиотеке Конгресса США в Вашингтоне.
Вы библиотекарь, и у вас нет никаких электронных устройств для помощи. Только:

📦 хранилище книг,
📑 индекс по названию,
✍️ индекс по автору,
🔑 индекс по ключевым словам.

Теперь рассмотрим примеры:

1️⃣ Самый простой запрос — «выдать все книги».
Подвозим вагоны и сгружаем в любом порядке.
Аналог: TABLE ACCESS FULL

2️⃣ Самый лёгкий запрос — найти книгу с известным местом хранения.
Просто идём и берём её, даже индекс не нужен.
Аналог: TABLE ACCESS BY ROWID

3️⃣ Запрос на книги автора — например, «Александр Сергеевич Пушкин».
Идём в индекс по авторам и получаем ссылки на его книги.
Аналог: INDEX RANGE SCAN (по диапазону).

Но что, если запрос будет таким:
автор пишет на индоевропейском языке,
фамилия начинается на «П»,
имя заканчивается на «р»,
жанр книги — сказка,
книга про рыбу.
Как быть?

Тут уже возникает несколько вариантов (планов) поиска:
искать авторов по первой букве,
искать книги по ключевым словам «сказка» и «рыба»,
пробовать разные комбинации,
или вовсе перебрать все книги.
👉 У вас уже 4 возможных плана запроса! И оптимизатору тоже приходится выбирать, какой путь будет наименее затратным.

⚙️ Оптимизатор:
каждому плану присваивает «стоимость»,
опирается на статистику (собранную заранее или из прошлых запросов),
иногда ошибается, если неправильно оценил путь поиска.

📊 Именно поэтому один и тот же запрос на разных базах может выполняться разными способами.

Если запрос сложный, нужно убедиться, что план не отличается от того, что был в тестовой среде.
Если отличается — применяем хинт или переписываем запрос, чтобы сделать его «понятнее» для оптимизатора.

💡 Главная мысль:
Чтобы понять, что такое план запроса, поставьте себя на место библиотекаря.
Продумайте, как бы вы искали данные вручную.
Именно так «думает» оптимизатор.

Дальше уже техника:
как подсказать оптимизатору, что он ошибается,
или как переписать запрос, чтобы сделать его проще для анализа.

Итого:
План запроса — это последовательность шагов.
Набор приёмов ограничен и часто повторяется.
Главное — понимать логику поиска.

Надеюсь, с понятием «план запроса» мы разобрались.
Дальше будем разбирать конкретные шаги и реальные примеры.

⚠️ Хотите проверить скрипты, но нет базы под рукой - онлайн-песочница
💎 Поддержка канала⁉️

👍 Палец вверх — продолжаем!
👎 Палец вниз — скучно и неинтересно.

Если есть вопросы, задавайте.
Если Вы опытный и заметили ошибку и/или неточность пишите тоже.
Если же ни то, ни другое, то всё равно пишите. 👇
#️⃣ #SQLOptimization
👍27
🎵 «Иду с дружком, гляжу — стоят
Они стояли молча в ряд
Они стояли молча в ряд
Их было восемь.
Со мною — нож, решил я: что ж
Меня так просто не возьмёшь.
Держитесь, гады! Держитесь, гады!» — Владимир Высоцкий

Сегодня суббота — задачи разбирать будем позже.
А пока продолжаем рубрику «Полезные ресурсы».

🖥️ Тема выпуска: Телеграм-каналы и чаты

Поскольку я блогер, то волей-неволей приходится изучать «конкурирующие фирмы».
Каналы, как и люди, — все разные:
кто-то с отличными постами, но навязчивой рекламой;
кто-то — с классным оформлением, но скучным контентом.

📢 Уважаемые читатели!
Делюсь подборкой профильных каналов на русском языке,
а также списком чатов, которые я лично не читаю,
поэтому оценить их качество не берусь — чаты просто не моё 🙂

🔧 Каналы
https://t.me/dbbooks (16.3k 👨‍💻) — крутой ресурс с множеством профильных книг
https://t.me/seniorsql (15.8k) — образовательный канал с ненавязчивой рекламой
https://t.me/sql_ready (12.4k) — профильный канал с интересным оформлением
https://t.me/sqlquestions (10.2k) — канал с интересной подачей и умеренной рекламой
https://t.me/sqlacademyofficial (8.9k) — образовательный канал с приятной подачей
https://t.me/db_in_it (8.2k) — инфо-канал с хорошим дизайном и лёгкой рекламой
https://t.me/database_info (8.1k) — образовательный канал с аккуратным оформлением
https://t.me/sqlprofi (5.2k) — интересный канал с оригинальной подачей
https://t.me/oracle_dbd (3.2k) — полезный контент, но реклама назойлива
https://t.me/pg_guru (2.8k) — профильный канал по PostgreSQL
https://t.me/sql_oracle_databases (1.8k) — образовательный ресурс по SQL и Oracle

💬 Чаты
https://t.me/sql_beginner (4.8k)
https://t.me/PostgreSQL_1C_Linux (4.1k)
https://t.me/sql_ninja (4.1k)
https://t.me/oracle_ru (1.8k)
https://t.me/oracle_dba_ru (1.5k)
https://t.me/pg_probackup (564)
https://t.me/databaselabru (273)
https://t.me/postgresprochat (79)

📚 Предлагаю всем желающим ознакомиться и найти себе контент по душе.

💎 Поддержка канала⁉️

👨‍💻 Скорее всего, я что-то пропустил — какие интересные профильные ресурсы на русском знаете Вы? А может, есть классные источники на других языках? Делитесь в комментариях. 👇
#️⃣ #Tools
👍8🔥1
Затравка для следующего поста.😊 Знаете ли функцию, которая безопасно перекомпилирует инвалидные объекты🩼 всей базы Oracle в параллели🚀?
Anonymous Poll
26%
Да, знаю.
49%
Нет, не знаю.
26%
Инвалиды!? А что это?
🎵 «И посредине этого разгула
Я прошептал на ухо жениху -
И жениха, как будто ветром сдуло,-
Невеста, вон, рыдает наверху.» — Владимир Высоцкий

Накат и откат изменений в Oracle

Для управления изменениями в базе данных Oracle важно иметь чёткую стратегию наката и отката.
Ниже — модель, как это можно организовать на любой базе: тестовой или боевой.

🔹 Модель действий

1. Считаем количество инвалидных объектов до наката
2. Запускаем скрипт наката/отката изменений
3. Перекомпилируем всех "инвалидов"
4. Считаем количество инвалидов после наката и сравниваем
5. Если есть ошибки — протоколируем их в отдельный журнал, например: INVALID_ERRORS$TASK_NUMBER

📌 Эта модель подходит как для наката изменений, так и для отката.
Таблица фиксирует все текущие ошибки и позволяет легко отслеживать изменения.

🔹 Создание таблицы ошибок
CREATE TABLE INVALID_ERRORS$TASK_NUMBER AS 
SELECT
o.owner,
o.object_type,
o.object_name,
e.sequence,
e.line,
e.position,
e.text
FROM
dba_objects o
INNER JOIN
dba_errors e
ON o.object_name = e.name AND o.owner = e.owner
WHERE
o.status = 'INVALID'
AND o.object_type IN ('PROCEDURE', 'FUNCTION', 'PACKAGE', 'PACKAGE BODY', 'VIEW', 'TRIGGER')
ORDER BY
o.owner,
o.object_type,
o.object_name,
e.sequence;

🔹 Параллельная перекомпиляция объектов
После наката/отката рекомендуется использовать UTL_RECOMP для перекомпиляции всех объектов в параллели, чтобы база была в рабочем состоянии:
BEGIN
SYS.UTL_RECOMP.recomp_parallel(
threads => 4
);
END;
/

Вот ссылка на официальную документацию UTL_RECOMP.

🔹 Итог
Всегда фиксируйте текущее состояние базы перед накатом.
Используйте таблицу ошибок для логирования ошибок.
Имейте скрипт отката на случай непредвиденных проблем.
После наката/отката запускайте параллельную перекомпиляцию объектов, чтобы минимизировать INVALID.

⚠️ Хотите проверить скрипты, но нет базы под рукой - онлайн-песочница
💎 Поддержка канала⁉️

👍 Палец вверх — интересно!
👎 Палец вниз — мне это не нужно, и потому скучно!

Интересна Ваша обратная связь - пишите тут 👇.
#️⃣ #Cases #SQL #Oracle #PLSQL
👍91👎1
🚀 Хотите прокачать SQL, но базы под рукой нет?
У моего коллеги Славы Рожнева — автора курса SQL в Яндексе — есть два ресурса:

🔹 sqltest.online — готовые задачи и тесты для практики.
🔹 sqlize.online — онлайн-песочница для запросов прямо в браузере.

📊 Отличный вариант, чтобы тренироваться в любом месте и в любое время!
👍5🔥1
🎵 «Один говорил: "Нам свобода - награда:
Мы поезд, куда надо ведем".
Другой говорил: "Задаваться не надо.
Как сядем в него, так и сойдем".» — Андрей Макаревич

📌 Разбор задачи: Вычисление n-го числа Фибоначчи через динамическое программирование
Представьте себе, что вы строите лестницу чисел.
Каждый новый шаг — это сумма двух предыдущих.
И вот приходит администратор и говорит: «Посчитайте десятый Фибоначчи!»

Что делает скрипт?
1. Создаёт «лестницу» чисел (массив).
2. Заполняет первые два шага единицами.
3. Каждый следующий шаг — это сумма двух предыдущих.
4. В конце вы получаете нужное число и всю последовательность целиком.

DECLARE
TYPE matrix_tab IS TABLE OF INTEGER;

fibMatrix matrix_tab := matrix_tab ();

fibonachiAmount INTEGER;

PROCEDURE get_fibonachi_matrix (index# IN INTEGER, matrix IN OUT NOCOPY matrix_tab, fibonachi_amount OUT INTEGER) IS
BEGIN
IF matrix.COUNT >= index# + 1 THEN
fibonachi_amount := matrix (index# + 1);
RETURN;
END IF;

IF index# >= 0 THEN
matrix.EXTEND;
matrix (matrix.COUNT) := 1;

IF index# = 0 THEN
fibonachi_amount := 1;
RETURN;
END IF;
END IF;

IF index# >= 1 THEN
matrix.EXTEND;
matrix (matrix.COUNT) := 1;

IF index# = 1 THEN
fibonachi_amount := 1;
RETURN;
END IF;
END IF;

FOR i IN 3 .. index# + 1 LOOP
matrix.EXTEND;
matrix (matrix.COUNT) := matrix (matrix.COUNT - 1) + matrix (matrix.COUNT - 2);
END LOOP;

fibonachi_amount := matrix (index# + 1);
END get_fibonachi_matrix;

PROCEDURE print_matrix (matrix IN OUT NOCOPY matrix_tab) IS
str VARCHAR2 (100);
BEGIN
FOR i IN 1 .. matrix.COUNT LOOP
str := str || ' ' || TO_CHAR (matrix (i));
END LOOP;

DBMS_OUTPUT.put_line (str);
END print_matrix;
BEGIN
get_fibonachi_matrix (index# => 10, matrix => fibMatrix, fibonachi_amount => fibonachiAmount);

DBMS_OUTPUT.put_line ('fibonachiAmount = ' || TO_CHAR (fibonachiAmount));

print_matrix (matrix => fibMatrix);
END;

💡 Как работает:
Первые два числа всегда 1.
Каждый новый элемент — это сумма двух предыдущих.
В итоге получаем как конкретное число, так и всю последовательность.

👍 Палец вверх — есть желание изучать алгоритмы.
👎 Палец вниз — хватит.

⚠️ Хотите проверить скрипты, но нет базы под рукой - онлайн-песочница
💎 Поддержка канала⁉️

💬 Что скажет "купечество"?
#️⃣ #SQL #Oracle #PLSQL
👍7👎3
🎵 «А я иду, шагаю по Москве,
И я ещё пройти смогу —
Солёный Тихий океан
И тундру, и тайгу…» — Сергей Никитин

📌 Разбор задачи: Разделение строки на слова

Иногда жизнь — как длинная дорога,
и каждое слово в строке — это шаг.
Одни шаги короткие, другие — длинные,
а между ними бывают остановки — пробелы.

🧠 Что делает скрипт?
1. Берёт исходную строку с кучей пробелов: ' 1 123 333 4567 '.
2. Проходит по ней символ за символом, как по дороге.
3. Когда встречает пробел — понимает: «О! шаг закончился».
4. Запоминает всё, что было между пробелами, — это и есть слово.
5. В конце выводит, сколько «шагов» пройдено и какие именно.

DECLARE
str VARCHAR2 (4000) := ' 1 123 333 4567 ';

TYPE str_tab IS TABLE OF VARCHAR2 (4000);

strs str_tab := str_tab ();

indexStartWord INTEGER := 0;
BEGIN
FOR index# IN 1 .. LENGTH (str)
LOOP
IF SUBSTR (str, index#, 1) = ' ' AND indexStartWord > 0
THEN
--DBMS_OUTPUT.put_line ('HERE');
strs.EXTEND;
strs (strs.COUNT) :=
SUBSTR (str, indexStartWord, index# - indexStartWord);
indexStartWord := 0;
END IF;

IF SUBSTR (str, index#, 1) != ' '
AND SUBSTR (str, index# - 1, 1) = ' '
THEN
indexStartWord := index#;
END IF;

--DBMS_OUTPUT.put_line (TO_CHAR (indexStartWord));
END LOOP;

-- print result
DBMS_OUTPUT.put_line (' ');
DBMS_OUTPUT.put_line ('word amount = ' || TO_CHAR (strs.COUNT));

FOR i IN 1 .. strs.COUNT
LOOP
DBMS_OUTPUT.put_line (strs (i));
END LOOP;

strs.delete;
END;

💡 Как работает:
Код идёт по строке, как путешественник по маршруту.
Каждый пробел — это остановка,
а всё между остановками — очередное слово.

Из ' 1 123 333 4567 ' получается маршрут:
1  
123
333
4567

⚠️ Хотите проверить скрипты, но нет базы под рукой - онлайн-песочница
💎 Поддержка канала⁉️

👍 Палец вверх — если есть контакт
👎 Палец вниз — если это "сало" достало.

💬 Пишите, и Ваше слово будет прочитано! 👇.
#️⃣ #SQL #Oracle #PLSQL
👍91👎1
🎵 «А не спеть ли мне песню о любви
А не выдумать ли новый жанр
Попопсовей мотив и стихи
И всю жизнь получать гонорар» — Чиж

📚 Серия «Оптимизация SQL-запросов». Планы запросов (одна таблица): часть 1

Продолжим аналогию с библиотекой — ведь она интуитивно понятна.
Я создал таблицу BOOKS, спроектированную весьма плохо — и это намеренно.

Зачем? Чтобы показать, как работает оптимизатор и какие бывают планы выполнения запросов при обращении к одной таблице.
CREATE TABLE books (
inventory_number NUMBER NOT NULL,
book_name VARCHAR2(500) NOT NULL,
language VARCHAR2(100) NOT NULL,
book_genre VARCHAR2(100) NOT NULL,
author_fullname VARCHAR2(100),
author_firstname VARCHAR2(100),
author_lastname VARCHAR2(100),
author_birthday DATE,
status_book VARCHAR2(50),
book_text CLOB NOT NULL
);


📖 Таблица денормализована, индексов слишком много, констрейнов нет — всё специально.
Цель — рассмотреть, как Oracle строит планы на одной таблице.

🔍 Что у нас есть

Мы — библиотекарь (оптимизатор).
Перед нами хранилище из миллиарда книг 📦

Созданы индексы:
CREATE UNIQUE INDEX books_u01 ON books (inventory_number);              -- уникальный номер книги
CREATE INDEX books_i01 ON books (book_name); -- по названию
CREATE INDEX books_i02 ON books (language); -- по языку
CREATE INDEX books_i03 ON books (book_genre); -- по жанру
CREATE INDEX books_i04 ON books (book_name, book_genre, language); -- составной
CREATE INDEX books_i05 ON books (author_fullname); -- по ФИО автора
CREATE INDEX books_i06 ON books (author_lastname, author_firstname); -- по фамилии и имени
CREATE INDEX books_i07 ON books (author_birthday); -- по дате рождения
CREATE INDEX books_i08 ON books (status_book); -- по статусу (Available, Checked Out, Reserved, Repaired)

Смотрите, в ORACLE много различных индексов и все они так или иначе применяются, но база это B-tree индексы (B - это не значит бинарный, а значит сбалансированный).
Абсолютное большинство индексов, которые Вы будете встречать, это нормальные B-tree индексы.
Экзотика полезна, но её надо изучать в контексте.

Заполним таблицу тестовыми данными:
INSERT ALL
INTO books (inventory_number, book_name, language, book_genre, author_fullname, author_firstname, author_lastname, author_birthday, status_book, book_text)
VALUES (1, 'War and Peace', 'English', 'Historical Novel', 'Leo Tolstoy', 'Leo', 'Tolstoy',
TO_DATE('09-09-1828', 'DD-MM-YYYY'), 'Available',
'Long philosophical novel about war and human destiny.')
INTO books VALUES (2, 'Crime and Punishment', 'English', 'Psychological Fiction',
'Fyodor Dostoevsky', 'Fyodor', 'Dostoevsky',
TO_DATE('11-11-1821', 'DD-MM-YYYY'), 'Checked Out',
'Story of guilt, morality, and redemption.')
INTO books VALUES (3, 'Pride and Prejudice', 'English', 'Romance',
'Jane Austen', 'Jane', 'Austen',
TO_DATE('16-12-1775', 'DD-MM-YYYY'), 'Available',
'A classic story about manners, marriage, and social class.')
INTO books VALUES (4, 'Les Misérables', 'French', 'Historical Novel',
'Victor Hugo', 'Victor', 'Hugo',
TO_DATE('26-02-1802', 'DD-MM-YYYY'), 'Reserved',
'Epic tale of injustice, revolution, and redemption.')
INTO books VALUES (5, 'Faust', 'German', 'Tragedy',
'Johann Wolfgang von Goethe', 'Johann', 'Goethe',
TO_DATE('28-08-1749', 'DD-MM-YYYY'), 'Available',
'A scholar makes a pact with the devil in search of knowledge.')
INTO books VALUES (6, 'Don Quixote', 'Spanish', 'Adventure',
'Miguel de Cervantes', 'Miguel', 'Cervantes',
TO_DATE('29-09-1547', 'DD-MM-YYYY'), 'Checked Out',
'A nobleman loses his sanity and becomes a wandering knight.')
SELECT * FROM dual;

#️⃣ #SQLOptimization #SQL #Oracle
📚 Планы запросов (одна таблица): часть 2

1️⃣ Полное чтение таблицы
SELECT * FROM books;


 Plan Hash Value  : 2688610195 

---------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost | Time |
---------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 2563 | 3 | 00:00:01 |
| 1 | TABLE ACCESS FULL | BOOKS | 1 | 2563 | 3 | 00:00:01 |
---------------------------------------------------------------------

Notes
-----
- Dynamic sampling used for this statement ( level = 2 )


📄 План: TABLE ACCESS FULL — читаем всю таблицу от начала до конца.
Oracle оценивает объём, строки и стоимость выполнения (Cost = 3).

💡 Если нет статистики — оптимизатор делает dynamic sampling.
Чтобы помочь ему, собираем статистику:
BEGIN 
DBMS_STATS.GATHER_TABLE_STATS (USER, 'BOOKS');
END;


После этого оценки становятся точнее - 6 записей против 1.
 Plan Hash Value  : 2688610195 

---------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost | Time |
---------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 6 | 2928 | 3 | 00:00:01 |
| 1 | TABLE ACCESS FULL | BOOKS | 6 | 2928 | 3 | 00:00:01 |
---------------------------------------------------------------------


2️⃣ Покрытые запросы (INDEX FULL SCAN)
Нужно получить все инвентарные номера:
SELECT inventory_number FROM books;

📈 Oracle использует индекс BOOKS_U01, не трогая таблицу.
Такой запрос называют покрытым — все нужные данные уже есть в индексе.

⚠️ Поэтому избегайте SELECT * — всегда указывайте только нужные колонки!

3️⃣ Самый быстрый запрос 🚀
SELECT * FROM books WHERE rowid = ...;


 Plan Hash Value  : 2667597667 

------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost | Time |
------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 244 | 1 | 00:00:01 |
| 1 | TABLE ACCESS BY USER ROWID | BOOKS | 1 | 244 | 1 | 00:00:01 |
------------------------------------------------------------------------------


🪶 ROWID хранит физическое местоположение строки.
Oracle идёт прямо к нужному блоку, минуя индексы.

#️⃣ #SQLOptimization #SQL #Oracle
2
📚 Планы запросов (одна таблица): часть 3

4️⃣ Доступ по уникальному индексу
SELECT * FROM books WHERE inventory_number = 1;


 Plan Hash Value  : 6300684 

------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost | Time |
------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 244 | 1 | 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID | BOOKS | 1 | 244 | 1 | 00:00:01 |
| * 2 | INDEX UNIQUE SCAN | BOOKS_U01 | 1 | | 0 | 00:00:01 |
------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
------------------------------------------
* 2 - access("INVENTORY_NUMBER"=1)


📘 План показывает:
INDEX UNIQUE SCAN по BOOKS_U01
затем TABLE ACCESS BY INDEX ROWID
То есть сначала Oracle ищет ROWID в индексе, потом берёт саму книгу.

5️⃣ Доступ по неуникальному индексу
SELECT * FROM books WHERE book_name = 'War and Peace';

 Plan Hash Value  : 480843298 

--------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost | Time |
--------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 244 | 2 | 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID BATCHED | BOOKS | 1 | 244 | 2 | 00:00:01 |
| * 2 | INDEX RANGE SCAN | BOOKS_I01 | 1 | | 1 | 00:00:01 |
--------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
------------------------------------------
* 2 - access("BOOK_NAME"='War and Peace')


🧭 План: INDEX RANGE SCAN
Даже если значение одно, Oracle предполагает, что книг с таким названием может быть несколько.
💬 Разница между INDEX UNIQUE SCAN и INDEX RANGE SCAN колоссальная по скорости.
Если можете писать точные фильтры по уникальному индексу — делайте это!

Пример:
SELECT * FROM books
WHERE inventory_number IN (1, 2, 3);
SELECT * FROM books
WHERE inventory_number >= 1 AND inventory_number <= 3;


Результат этих запросов идентичен, но план запросов по скорости отличается "как небо и земля".
В одном случае будет INDEX UNIQUE SCAN, в другом INDEX RANGE SCAN.
Если используется INDEX UNIQUE SCAN — это значительно быстрее, чем диапазонное условие.

6️⃣ Доступ по неполному индексу
SELECT * FROM books
WHERE language = 'Russian' AND book_genre = 'Novel';

 Plan Hash Value  : 1417276700 

--------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost | Time |
--------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 244 | 2 | 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID BATCHED | BOOKS | 1 | 244 | 2 | 00:00:01 |
| * 2 | INDEX SKIP SCAN | BOOKS_I04 | 1 | | 1 | 00:00:01 |
--------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
------------------------------------------
* 2 - access("BOOK_GENRE"='Novel' AND "LANGUAGE"='Russian')
* 2 - filter("LANGUAGE"='Russian' AND "BOOK_GENRE"='Novel')


📊 План: INDEX SKIP SCAN
Oracle использует составной индекс (book_name, book_genre, language),
пропуская первое поле (book_name).

⚠️ Если видите в плане INDEX SKIP SCAN, это сигнал проверить:
Правильные ли созданы индексы на таблице?
Не в том ли порядке созданы поля составного индекса?

7⃣ Напоследок: есть ещё INDEX FAST FULL SCAN,
но о нём как-нибудь в другой раз. Это уже разбор не для новичков.

#️⃣ #SQLOptimization #SQL #Oracle
👍2