Любое приложение, работающее с базами данных, рано или поздно достигает точки, когда разрозненные 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()orsp_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-кодом. И то, и другое требует систематического анализа, а не коллективных знаний.