Хранимые процедуры в управлении базами данных

Хранимые процедуры: как они работают и как их эффективно использовать.

Любое приложение, работающее с базами данных, рано или поздно достигает точки, когда разрозненные SQL-запросы, распределенные по всему коду приложения, становятся проблемой обслуживания. Один и тот же сложный оператор объединения встречается в трех разных сервисах. Бизнес-логика, которая должна находиться на уровне базы данных, проникает в приложение. Политики безопасности, которые должны применяться на уровне данных, вместо этого применяются непоследовательно кодом приложения, который можно обойти. Хранимые процедуры решают эту проблему, перемещая многократно используемую, чувствительную к безопасности и критически важную для производительности логику SQL в базу данных, где ею можно управлять, версионировать, защищать и оптимизировать независимо от приложений, которые ее вызывают.

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

Перед выполнением каждого изменения в базе данных необходимо предварительно проверить его параметры.

SMART TS XL отображает зависимости хранимых процедур одновременно в SQL, COBOL, Java и Python.

Подробнее

Что такое хранимая процедура?

Хранимая процедура — это предварительно скомпилированная подпрограмма, хранящаяся в реляционной базе данных и вызываемая по имени с необязательными параметрами. Механизм базы данных компилирует её один раз, кэширует план выполнения и повторно использует этот план при каждом последующем вызове, избегая цикла «анализ-компиляция-оптимизация», который необходим для выполнения произвольных SQL-запросов каждый раз.

SQL

-- SQL Server: basic stored procedure
CREATE PROCEDURE GetCustomerOrders
    @CustomerId INT,
    @StartDate  DATE,
    @EndDate    DATE
AS
BEGIN
    SET NOCOUNT ON;

    SELECT
        o.OrderId,
        o.OrderDate,
        o.TotalAmount,
        o.Status
    FROM Orders o
    WHERE o.CustomerId = @CustomerId
      AND o.OrderDate BETWEEN @StartDate AND @EndDate
    ORDER BY o.OrderDate DESC;
END;
GO

-- Calling the procedure
EXEC GetCustomerOrders
    @CustomerId = 12345,
    @StartDate  = '2026-01-01',
    @EndDate    = '2026-06-30';

Приложение никогда не записывает SQL-запросы напрямую. Оно вызывает... GetCustomerOrders с параметрами. Остальное обрабатывает база данных.

Хранимая процедура, представление и функция.

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

объектReturnsПринимает параметрыМожно изменять данныеПлан выполнения кэшированДля каких задач
Хранимая процедураНаборы результатов, выходные параметры, коды возвратаДаДаДаСложная логика, операции DML, обеспечение безопасности.
ДеталиЕдиный набор результатов (например, таблица)НетНет (обычно)ЧастичныйУпрощение запросов SELECT, безопасность на уровне столбцов.
Скалярная функцияОдно значениеДаНетНетВычисления повторно используются в списках SELECT.
Таблично-значная функцияНабор результатовДаНетЧастичныйПараметризованные представления, вычисления, возвращающие множество

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

Четыре основных преимущества

Производительность: предварительная компиляция и кэширование планов.

Когда SQL Server, PostgreSQL или Oracle получает вызов хранимой процедуры, он проверяет, существует ли для этой процедуры кэшированный план выполнения. Если существует, процедура выполняется немедленно. В противном случае, она компилирует процедуру, генерирует план выполнения, кэширует его и выполняет. При всех последующих вызовах кэшированный план используется повторно.

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

SQL

-- Demonstrating plan reuse in SQL Server
-- Check if a plan exists for a procedure
SELECT
    qs.execution_count,
    qs.total_elapsed_time / qs.execution_count AS avg_elapsed_ms,
    qs.total_logical_reads / qs.execution_count AS avg_logical_reads,
    SUBSTRING(qt.text, 1, 100) AS procedure_text
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
WHERE qt.text LIKE '%GetCustomerOrders%'
ORDER BY qs.execution_count DESC;

Второе преимущество в производительности — снижение сетевого трафика . Вместо отправки сложного SQL-запроса из приложения в базу данных из 30 строк при каждом обращении, приложение отправляет короткий вызов процедуры. Сетевая нагрузка минимальна. База данных выполняет ресурсоемкие вычисления на стороне сервера и возвращает только результирующий набор.

Безопасность: Ограничение прямого доступа к таблицам.

Именно это преимущество и ищут в базе данных SC: «как использовать хранимые процедуры для ограничения прямого доступа к данным и повышения безопасности базы данных». Механизм прост и эффективен.

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

SQL

-- Create a role for application users
CREATE ROLE AppReadRole;

-- Grant execute on the procedure
GRANT EXECUTE ON GetCustomerOrders TO AppReadRole;

-- Deny direct table access
DENY SELECT ON Orders TO AppReadRole;
DENY SELECT ON Customers TO AppReadRole;

-- The user can now:
--   EXEC GetCustomerOrders @CustomerId=123, ... -> works
--   SELECT * FROM Orders WHERE ... -> ACCESS DENIED

Второе преимущество в плане безопасности — предотвращение SQL-инъекций . Хранимые процедуры, использующие параметризованные входные данные, а не динамическое построение SQL-запросов, по своей природе защищены от SQL-инъекций. Значение параметра рассматривается как литерал, а не как SQL-код.

SQL

-- VULNERABLE: dynamic SQL built from user input
DECLARE @sql NVARCHAR(500);
SET @sql = 'SELECT * FROM Customers WHERE Name = ''' + @UserInput + '''';
EXEC(@sql);
-- An attacker can inject: '; DROP TABLE Customers; --

-- SAFE: parameterized stored procedure
CREATE PROCEDURE GetCustomerByName
    @CustomerName NVARCHAR(100)
AS
BEGIN
    SELECT CustomerId, Name, Email
    FROM Customers
    WHERE Name = @CustomerName;  -- @CustomerName is a literal, not SQL
END;

Осторожно: Хранимая процедура, которая внутренне формирует динамический SQL-запрос, используя EXEC() or sp_executesql Конкатенация строк из пользовательских входных данных так же уязвима, как и динамический SQL на уровне приложения. Параметризация должна распространяться на любой динамический SQL, создаваемый внутри процедуры.

Поддерживаемость: одно изменение — и все приложения обновлены.

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

SQL

-- Before: discount logic duplicated in three application services
-- After: centralized in one stored procedure
CREATE PROCEDURE CalculateOrderTotal
    @OrderId INT,
    @DiscountedTotal DECIMAL(10,2) OUTPUT
AS
BEGIN
    DECLARE @Subtotal DECIMAL(10,2);
    DECLARE @CustomerTier NCHAR(1);

    SELECT @Subtotal     = SUM(li.Quantity * li.UnitPrice),
           @CustomerTier = c.Tier
    FROM OrderLineItems li
    JOIN Orders o ON o.OrderId = li.OrderId
    JOIN Customers c ON c.CustomerId = o.CustomerId
    WHERE li.OrderId = @OrderId
    GROUP BY c.Tier;

    -- Business rule: Gold customers get 15%, Silver get 8%, Standard get 0%
    SET @DiscountedTotal = @Subtotal * CASE @CustomerTier
        WHEN 'G' THEN 0.85
        WHEN 'S' THEN 0.92
        ELSE 1.00
    END;
END;

Изменяйте процент скидки в одном месте. Все три сервиса приложений, которые вызывают CalculateOrderTotal немедленно отразить новые тарифы.

Инкапсуляция с выходными параметрами и обработкой ошибок.

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

SQL

CREATE PROCEDURE InsertOrder
    @CustomerId  INT,
    @OrderDate   DATE,
    @NewOrderId  INT OUTPUT,
    @StatusCode  INT OUTPUT
AS
BEGIN
    SET NOCOUNT ON;
    BEGIN TRY
        BEGIN TRANSACTION;

        INSERT INTO Orders (CustomerId, OrderDate, Status)
        VALUES (@CustomerId, @OrderDate, 'PENDING');

        SET @NewOrderId = SCOPE_IDENTITY();
        SET @StatusCode = 0;  -- success

        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        ROLLBACK TRANSACTION;
        SET @StatusCode = ERROR_NUMBER();
        SET @NewOrderId = -1;
    END CATCH;
END;

-- Usage
DECLARE @OrderId INT, @Status INT;
EXEC InsertOrder
    @CustomerId = 5001,
    @OrderDate  = '2026-07-15',
    @NewOrderId = @OrderId OUTPUT,
    @StatusCode = @Status OUTPUT;

IF @Status <> 0
    PRINT 'Insert failed with error: ' + CAST(@Status AS VARCHAR);
ELSE
    PRINT 'Created order: ' + CAST(@OrderId AS VARCHAR);

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

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

SQL

-- Enable actual execution plan in SQL Server
-- Then run:
EXEC GetCustomerOrders
    @CustomerId = 12345,
    @StartDate  = '2026-01-01',
    @EndDate    = '2026-06-30';

-- Check for parameter sniffing issues
-- (cached plan was optimized for a different parameter distribution)
EXEC GetCustomerOrders
    @CustomerId = 99999,  -- rare customer with very few orders
    @StartDate  = '2026-01-01',
    @EndDate    = '2026-06-30'
WITH RECOMPILE;  -- forces fresh plan for this call

Проблема определения параметров (parameter sniffing) является наиболее распространенной проблемой производительности хранимых процедур. SQL Server кэширует план выполнения, сгенерированный для первого набора параметров, с которыми вызывается процедура. Если последующие вызовы используют очень разные значения параметров (например, клиент с 50 000 заказов против клиента с 2 заказами), кэшированный план может быть крайне неоптимальным для этих значений.

Стратегии смягчения последствий: OPTIMIZE FOR подсказка для оптимизации под репрезентативное значение параметра; WITH RECOMPILE на уровне процедуры для генерации нового плана при каждом вызове (дорогостоящий, но эффективный метод, когда распределение параметров сильно варьируется); присвоение локальных переменных в начале процедуры для предотвращения перехвата параметров:

SQL

CREATE PROCEDURE GetCustomerOrders
    @CustomerId INT,
    @StartDate  DATE,
    @EndDate    DATE
AS
BEGIN
    -- Local variable trick: prevents parameter sniffing
    DECLARE @LocalCustomerId INT = @CustomerId;
    DECLARE @LocalStart      DATE = @StartDate;
    DECLARE @LocalEnd        DATE = @EndDate;

    SELECT o.OrderId, o.OrderDate, o.TotalAmount
    FROM Orders o
    WHERE o.CustomerId = @LocalCustomerId
      AND o.OrderDate BETWEEN @LocalStart AND @LocalEnd;
END;

Передовые методы: рабочий контрольный список

Речь идёт не об общих принципах, а о методах, обеспечивающих поддержку хранимых процедур в масштабах предприятия:

Название и организация

  • Используйте единообразную систему именования: usp_ префикс для хранимых процедур пользователя, sp_ зарезервировано для системных процедур
  • Называйте процедуры с помощью глагола и существительного: GetCustomerOrders, InsertPaymentRecord, UpdateInventoryCount
  • Связанные с группой процедуры в схеме: Sales.GetCustomerOrders, Inventory.UpdateStock

Структура кода

  • Начинайте каждую процедуру с SET NOCOUNT ON для подавления сообщений о количестве строк, которые клиенты могут неправильно истолковать
  • Используйте BEGIN TRY / BEGIN CATCH блоки с явными BEGIN TRANSACTION / COMMIT / ROLLBACK
  • Избегайте использования курсоров для операций над множествами, по возможности переписывайте SQL-запросы на основе множеств.
  • Не используйте SELECT *укажите названия всех столбцов, возвращаемых процедурой.

Безопасность.

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

Эффективности

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

Документация

  • Добавьте заголовок к каждой процедуре: назначение, параметры, возвращаемые значения, автор, дата последнего изменения.
  • Документируйте бизнес-правила, закодированные в логике процедуры, не только то, что делает SQL-запрос, но и почему.

Управление зависимостями хранимых процедур

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

При изменении типа столбца таблицы необходимо протестировать каждую процедуру, которая ссылается на этот столбец. При изменении формата вывода процедуры необходимо проверить каждый вызывающий её компонент. При рассмотрении вопроса о модификации процедуры полный набор вызывающих компонентов определяет масштаб изменений и необходимый объем регрессионного тестирования.

Важные типы зависимостей:

  • Зависимости объектов: таблицы, представления, функции и другие процедуры, на которые ссылается данная процедура
  • Зависимости вызывающего кода: код приложения, другие хранимые процедуры и запланированные задания, которые вызывают эту процедуру.
  • Зависимости схемы: таблицы и определения столбцов, которым должны соответствовать типы параметров процедуры и списки SELECT.
  • Зависимости транзакций: процедуры, которые разделяют область действия транзакции со своими вызывающими или друг с другом

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

Как SMART TS XL Управляет зависимостями хранимых процедур в масштабе предприятия.

SMART TS XLАвтора статический анализ кода Анализирует хранимые процедуры SQL совместно с программами COBOL, сервисами Java, конвейерами Python и другими компонентами, взаимодействующими с той же базой данных. Единый анализ создает кроссъязыковую структурную модель: не только зависимости SQL-SQL внутри базы данных, но и всю цепочку от кода приложения через хранимые процедуры до базовых таблиц и обратно.

Функция сопоставления зависимостей приложения позволяет построить полный граф вызывающих процессов: какие программы на COBOL используют встроенный SQL для чтения данных из таблиц, принадлежащих хранимым процедурам, какие службы Java вызывают хранимые процедуры через JDBC, какие пакетные задания JCL запускают утилиты базы данных, которые выполняют хранимые процедуры. При изменении сигнатуры или поведения хранимой процедуры карта зависимостей показывает все вызывающие процессы на всех языках программирования, а также полный объем информации, которую необходимо протестировать перед внедрением изменений в рабочую среду.

анализ воздействия Благодаря этой возможности карта зависимостей становится пригодной для планирования изменений: можно предложить изменение в CalculateOrderTotal и получить исчерпывающий список всех компонентов, которые его вызывают, всех таблиц, которые он читает и записывает, и всех последующих процедур, которые он запускает. Это превращает вопрос «что это сломает?» из упражнения в устоявшихся знаниях в структурированный, основанный на фактах отчет о масштабе проекта.

поиск на предприятии Благодаря этой возможности можно выполнять запросы ко всей модели зависимостей: находить каждую хранимую процедуру, которая читает данные из... Orders, каждый звонящий GetCustomerOrdersКаждая процедура, изменяющая определенный столбец, измеряется в секундах в базе данных любого размера.

Для команд, проводящих модернизация наследия в программах, где хранимые процедуры кодируют бизнес-логику, накопленную за десятилетия, и которую необходимо сохранить во время миграции. SMART TS XLАнализ предоставляет структурную документацию, которая позволяет извлечь логику и спланировать последовательность миграции.

Уровень базы данных, который оправдывает свою стоимость

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

Проблема не в использовании хранимых процедур, а в том, что они разрастаются в недокументированную, неучтенную сеть зависимостей, которую никто до конца не понимает. Хранимая процедура, выполняющая критически важные бизнес-вычисления, но не имеющая документированных вызывающих функций, заголовка с пояснением ее назначения и анализа влияния перед модификацией, является обузой, независимо от того, насколько хорошо она написана. Управление графом зависимостей так же важно, как и управление самим SQL-кодом. И то, и другое требует систематического анализа, а не коллективных знаний.