JavaRush /Курсы /SQL SELF /Оптимизация процедур с учетом транзакций: анализ производ...

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

SQL SELF
54 уровень , 3 лекция
Открыта

Когда вы разрабатываете процедуры, они часто становятся "сердцем" вашей базы данных, выполняя множество операций. Однако эти же процедуры могут быть "узким местом", особенно если:

  1. Они выполняют ненужные операции (например, часто обращаются к одним и тем же данным).
  2. Они неэффективно используют индексы.
  3. Выполняют слишком много операций внутри одной транзакции.

Как сказал один мудрый разработчик: "Ускорить плохо написанный код — как просить ленивого друга бегать быстрее". Поэтому оптимизация процедур — это не просто улучшение скорости, это улучшение самой основы!

Минимизация количества операций внутри транзакции

Каждая транзакция в PostgreSQL создает накладные расходы для обслуживания своих операций. Чем крупнее транзакция, тем дольше она удерживает блокировки и тем больше шансов на блокировки других пользователей. Чтобы минимизировать эти эффекты:

  1. Не объединяйте в одну транзакцию слишком много операций.
  2. Используйте EXCEPTION END, чтобы локально ограничивать изменения. Это полезно, если только часть операций требует отката.
  3. Разделяйте крупные транзакции на несколько меньших (если логика вашего приложения это позволяет).

Пример: разделение массовой вставки данных на "пакеты":

-- Пример: Процедура для пакетной загрузки с поэтапным коммитом
CREATE PROCEDURE batch_load()
LANGUAGE plpgsql
AS $$
DECLARE
    r RECORD;
    batch_cnt INT := 0;
BEGIN
    FOR r IN SELECT * FROM staging_table LOOP
        BEGIN
            INSERT INTO target_table (col1, col2) VALUES (r.col1, r.col2);
            batch_cnt := batch_cnt + 1;
        EXCEPTION
            WHEN OTHERS THEN
                -- Логируем ошибку; изменения по этому элементу будут откатаны
                INSERT INTO load_errors(msg) VALUES (SQLERRM);
        END;
        IF batch_cnt >= 1000 THEN
            COMMIT; -- фиксируем каждую 1000 операций
            batch_cnt := 0;
        END IF;
    END LOOP;
    COMMIT; -- финальный коммит
END;
$$;

Совет: не забывайте, что каждый COMMIT фиксирует изменения, поэтому убедитесь заранее, что разделение транзакции не нарушит целостность данных.

Использование индексов для ускорения запросов

Допустим, у нас есть таблица orders с миллионом записей, и вы часто делаете запросы по customer_id. Без индекса запрос будет сканировать все строки:

CREATE INDEX idx_customer_id ON orders(customer_id);

Теперь запросы, подобные этому:

SELECT * FROM orders WHERE customer_id = 42;

будут работать гораздо быстрее, избегая сканирования всей таблицы.

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

Анализ производительности с EXPLAIN ANALYZE

EXPLAIN показывает план выполнения запроса (как PostgreSQL собирается его выполнить), а ANALYZE добавляет реальную статистику выполнения (например, сколько времени ушло на выполнение). Вот типичный пример:

EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 42;

Как его использовать внутри процедуры?

Вы можете "разложить" сложные запросы вашей процедуры, выполнив их отдельно с EXPLAIN ANALYZE:

DO $$
BEGIN
    RAISE NOTICE 'Query Plan: %',
    (
        SELECT query_plan
        FROM pg_stat_statements
        WHERE query = 'SELECT * FROM orders WHERE customer_id = 42'
    );
END $$;

Пример анализа и улучшения

Изначальная процедура (медленная):

CREATE OR REPLACE FUNCTION update_total_sales()
RETURNS VOID AS $$
BEGIN
    UPDATE sales
    SET total = (
        SELECT SUM(amount)
        FROM orders
        WHERE orders.sales_id = sales.id
    );
END $$ LANGUAGE plpgsql;

Что происходит? Для каждой строки из таблицы sales выполняется подзапрос SUM(amount), что приводит к множеству операций. Это медленно.

Улучшенный вариант:

CREATE OR REPLACE FUNCTION update_total_sales()
RETURNS VOID AS $$
BEGIN
    UPDATE sales as s
    SET total = o.total_amount
    FROM (
        SELECT sales_id, SUM(amount) as total_amount
        FROM orders
        GROUP BY sales_id
    ) o
    WHERE o.sales_id = s.id;
END $$ LANGUAGE plpgsql;

Теперь подзапрос с SUM выполнится один раз, и все данные сразу обновятся.

Откаты данных при ошибках

Если что-то пошло не так внутри процедуры, вы можете откатить только часть транзакции. Например:

BEGIN
    -- Вставляем данные
    INSERT INTO inventory(product_id, quantity) VALUES (1, -5);
EXCEPTION
    WHEN OTHERS THEN
        -- Этот блок эквивалентен откату к внутреннему savepoint!
        RAISE WARNING 'Ошибка при обновлении данных: %', SQLERRM;
END;

Практика: реализация устойчивой процедуры обработки заказов

Допустим, ваша задача: обработать заказ. Если в процессе возникла ошибка (например, товара не хватает), заказ отменяется, а ошибка логируется.

CREATE OR REPLACE PROCEDURE process_order(p_order_id INT)
LANGUAGE plpgsql
AS $$
DECLARE
    v_in_stock INT;
BEGIN
    -- Проверяем остатки
    SELECT stock INTO v_in_stock FROM products WHERE id = p_order_id;

    BEGIN
        IF v_in_stock < 1 THEN
            RAISE EXCEPTION 'Нет товара на складе';
        END IF;
        UPDATE products SET stock = stock - 1 WHERE id = p_order_id;
        -- ... другие операции
    EXCEPTION
        WHEN OTHERS THEN
            -- Все изменения в этом блоке откатываются!
            INSERT INTO order_logs(order_id, log_message)
                VALUES (p_order_id, 'Ошибка обработки: ' || SQLERRM);
            RAISE NOTICE 'Ошибка обработки заказа: %', SQLERRM;
    END;

    -- Остальной код продолжается, если не было ошибок
    -- Можно логировать: заказ успешно обработан
END;
$$;
  • Даже при ошибке заказ не обработается, а лог появится в таблице order_logs.
  • При ошибке сработает внутренний savepoint, и вы не потеряете весь контекст.

Основные правила оптимизации и устойчивости процедур

  1. Используйте индексы для запросов внутри процедур.
  2. Разделяйте большие операции на небольшие пачки (batch), делайте поэтапную обработку.
  3. Умейте логировать ошибки — делайте отдельную таблицу для логов ошибок массовых операций.
  4. Для "частичных" откатов используйте только вложенные блоки с EXCEPTION.
  5. Не используйте ROLLBACK TO SAVEPOINT внутри PL/pgSQL — это вызовет ошибку синтаксиса.
  6. В процедурах используйте COMMIT/SAVEPOINT только при вызове в autocommit-режиме соединения!
  7. Анализируйте план выполнения тяжелых запросов (EXPLAIN ANALYZE) вне процедур, до интеграции.
2
Задача
SQL SELF, 54 уровень, 3 лекция
Недоступна
Разделение большой транзакции на пакеты
Разделение большой транзакции на пакеты
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Ra Уровень 35 Student
21 августа 2025
Можно было добавить инфу, как перехватывать разные исключения