SQL Server: not only SQL, асинхронний виклик команд

— Алло, швидка?!! У нас людина штопор проковтнула, що нам робити?!!
— Ну-у... Спробуйте відкривати виделкою!

Вітаю, спільното! Мене звати Володимир Жадан, я працюю з Microsoft SQL Server понад 15 років і хотів би поділитися думками щодо використання деяких його можливостей, які часто залишаються поза увагою розробників.

Вступ

Ця стаття призначена радше для розробників бекенду або архітекторів, бо, як правило, розробники/адміністратори баз даних знають про механізми, описані в статті. Як і в епіграфі, будемо намагатись застосувати інструменти не за призначенням, бо жоден з описаних механізмів не розроблявся саме для асинхронного виклику, але вони дозволяють його реалізувати.

До мене кілька разів зверталися розробники на мейнстрімних мовах з питанням, як можна в SQL Server реалізувати асинхронний запуск SQL-коду. Мене дивувало, чому з цим питанням звертаються саме вони, оскільки в цих мовах майже завжди є якісь async/await або їх аналоги, через які все це реалізується елементарно на рівні застосунку. Саме на тому в цих випадках і зупинялись.

Проте інколи виникає потреба запустити якийсь код асинхронно з-під SQL.

Звичайно, неважко асинхронне виконання реалізувати самотужки, наприклад, як запуск зовнішнього файлу через xp_cmdshell. Або створивши таблицю в ролі черги, куди поточний процес буде додавати рядки, в яких містимуться параметри завдання, і окремий процес для їх виконання (найпростіше — це процедура, або SQL-скрипт, запущені в SQL Server Agent або в сторонній програмі, які читають таблицю і виконують завдання).

Але в SQL Server є і власні механізми, які хоч і призначені для інших цілей, можуть використовуватись для асинхронного виклику. Два з них доволі широко описані в інтернеті, але я наведу приклади їх використання, щоб при бажанні їх спробувати не потрібно було ходити до інших джерел. Це запуск через Service Broker і через SQL Server Agent.

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

Створення оточення для тестування

Щоб продемонструвати дію та порівняти швидкість запуску механізмів, створимо для початку в якій-небудь базі (я створив окрему базу AsyncTest для цієї демонстрації) таблицю, куди будемо логувати роботу наших скриптів:

CREATE TABLE dbo.AsyncCallsLog(
	ExecutionId int IDENTITY(1,1) PRIMARY KEY CLUSTERED,
	AsyncMethod varchar(50) NOT NULL,
	StartMainScriptTime datetime2(3) NOT NULL DEFAULT (sysdatetime()),
	FinishMainScriptTime datetime2(3) NULL,
	FinishAsyncCallTime datetime2(3) NULL,
	[MainScriptDuration, ms] AS datediff(millisecond,StartMainScriptTime,FinishMainScriptTime),
	[TotalDuration, ms] AS datediff(millisecond,StartMainScriptTime,FinishAsyncCallTime),
	[AsyncEngineDuration, ms] AS datediff(millisecond,FinishMainScriptTime,FinishAsyncCallTime)-10000
)
GO

Тепер нам потрібна процедура, яка зареєструє в цій таблиці факт старту скрипта, запише назву механізму, який ми тестуємо і поверне значення первинного ключа для ідентифікації рядка, куди будемо писати час закінчення основної та «асинхронної» частини виконання коду:

CREATE PROCEDURE dbo.AsyncCallStart
	@AsyncMethod varchar(50), 
	@ExecutionId int OUTPUT
AS
INSERT INTO dbo.AsyncCallsLog
           (AsyncMethod)
     VALUES
           (@AsyncMethod)
SET @ExecutionId = @@IDENTITY
GO

І, нарешті процедуру, яку ми будемо запускати асинхронно. В неї вставлено паузу 10 секунд, щоб наочно побачити, що ця процедура закінчується після основного скрипта, і зазначити час закінчення, який ми використаємо для оцінки швидкодії механізму асинхронного запуску:

CREATE PROCEDURE dbo.AsyncCallExecute
	@ExecutionId int
AS
BEGIN

-- Pause for 10 seconds
WAITFOR DELAY '00:00:10';

UPDATE dbo.AsyncCallsLog
SET FinishAsyncCallTime = SYSDATETIME()
WHERE ExecutionId = @ExecutionId

END
GO

Можна було, звичайно, передавати як параметр текст SQL-інструкції для виконання, але задля безпеки не будемо робити аналог динамічного SQL.

Здається, все.

Service Broker

Service Broker — це вбудований механізм асинхронного обміну повідомленнями між базами або/та серверами SQL Server, який дозволяє надіслати повідомлення в чергу, яку потім обробляє спеціальний активатор або отримувач повідомлень. Працює всередині SQL Server, без потреби зовнішніх агентів чи додаткового коду.

Основні компоненти:

  1. Message Types — визначають формат повідомлень.
  2. Contracts — описують, які типи повідомлень можна надсилати між учасниками.
  3. Queues — черги для зберігання повідомлень.
  4. Services — логічні точки надсилання/отримання повідомлень.
  5. Activation Procedures — процедури, що автоматично запускаються при появі нового повідомлення в черзі.

Алгоритм роботи:

  1. Застосунок або T-SQL процедура відправляє повідомлення у queue.
  2. SQL Server зберігає повідомлення та гарантує його доставку.
  3. Коли в черзі з’являється повідомлення, може бути викликана activation procedure, яка обробляє його асинхронно.

Щоб реалізувати асинхронний запуск коду, потрібно створити кожен з основних компонентів.
Давайте спробуємо це.

Передусім перевіримо, чи увімкнено в базі Service Broker. Якщо ні, то увімкнемо.

-- 0. Enable Service Broker on the database if not already enabled
IF NOT EXISTS (
    SELECT * FROM sys.databases WHERE name = 'AsyncTest' AND is_broker_enabled = 1
)
BEGIN
    ALTER DATABASE [AsyncTest] SET ENABLE_BROKER WITH NO_WAIT;
END

Тепер створимо тип повідомлення, контракт, чергу, сервіс і процедуру активації:
-- 1. Create a message type
CREATE MESSAGE TYPE [AsyncProcMessage]
    VALIDATION = NONE;
GO

-- 2. Create a contract
CREATE CONTRACT [AsyncProcContract]
    ([AsyncProcMessage] SENT BY INITIATOR);
GO

-- 3. Create a queue
CREATE QUEUE [AsyncProcQueue];
GO

-- 4. Create a service
CREATE SERVICE [AsyncProcService]
    ON QUEUE [AsyncProcQueue]
    ([AsyncProcContract]);
GO

-- 5. Create the activation procedure
CREATE PROCEDURE [dbo].[AsyncProc_Activation]
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @conversation_handle UNIQUEIDENTIFIER,
            @message_body NVARCHAR(MAX),
            @message_type_name SYSNAME;

    WHILE (1 = 1)
    BEGIN
        WAITFOR (
            RECEIVE TOP(1)
                @conversation_handle = conversation_handle,
                @message_body = message_body,
                @message_type_name = message_type_name
            FROM [AsyncProcQueue]
        ), TIMEOUT 1000;

        IF @@ROWCOUNT = 0 BREAK;

        DECLARE @ExecutionId int = JSON_VALUE(@message_body, '$.ExecutionId');

        EXEC dbo.AsyncCallExecute @ExecutionId;

        END CONVERSATION @conversation_handle;
    END
END
GO

І додамо у визначення черги параметри активації:

-- 6. Set queue activation
ALTER QUEUE [AsyncProcQueue]
    WITH ACTIVATION (
        STATUS = ON,
        PROCEDURE_NAME = [dbo].[AsyncProc_Activation],
        MAX_QUEUE_READERS = 100,
        EXECUTE AS OWNER
    );
GO

І, власне, код для тестування:

-- 0. Get the next ExecutionId
DECLARE	@ExecutionId int

EXEC	[dbo].[AsyncCallStart]
		@AsyncMethod = 'Service Broker',
		@ExecutionId = @ExecutionId OUTPUT

SELECT	@ExecutionId as N'@ExecutionId'

-- 1. To execute the procedure asynchronously, send a message:
DECLARE @conversation_handle UNIQUEIDENTIFIER;
BEGIN DIALOG CONVERSATION @conversation_handle
    FROM SERVICE [AsyncProcService]
    TO SERVICE   'AsyncProcService'
    ON CONTRACT  [AsyncProcContract]
    WITH ENCRYPTION = OFF; -- If your database does not have a master key, you must disable encryption

DECLARE @message_body NVARCHAR(MAX) = (
    SELECT @ExecutionId AS ExecutionId
    FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
); -- Set parameters as XML/JSON here

SEND ON CONVERSATION @conversation_handle
    MESSAGE TYPE [AsyncProcMessage]
    (@message_body); -- Optionally pass parameters as XML/JSON here

-- Conversation will be ended by the activation procedure

-- 2. Update the main script finish time
UPDATE dbo.AsyncCallsLog
SET FinishMainScriptTime = SYSDATETIME()
WHERE ExecutionId = @ExecutionId
GO

Процедура AsyncCallStart починає тест, записуючи в таблицю AsyncCallsLog рядок з часом початку старту і назвою механізму Service Broker і повертає параметр @ExecutionId, значення якого через тіло повідомлення @message_body в форматі JSON ми передаємо для виклику процедури AsyncCallExecute.

Запустимо цей код кілька разів, щоб зібрати якусь статистику щодо швидкості виконання, бо іноді перший запуск після довгої перерви проходить повільніше.

SQL Server Agent

Тут є два шляхи — створювати нове завдання (job) або запустити існуюче, тільки в цьому випадку, якщо воно вже запущене, повторно викликати його не вдасться.

Приклад коду, який для асинхронного виклику створює нову джобу:

-- Create a SQL Server Agent Job that deletes itself after successful execution

-- 0. Get the next ExecutionId
DECLARE @ExecutionId int

EXEC    [dbo].[AsyncCallStart]
        @AsyncMethod = 'SQL Server Agent (new job)',
        @ExecutionId = @ExecutionId OUTPUT

SELECT  @ExecutionId as N'@ExecutionId'

-- 1. Create the job
DECLARE @command NVARCHAR(MAX) = N'EXEC dbo.AsyncCallExecute @ExecutionId = ' + CAST(@ExecutionId AS NVARCHAR(10));

DECLARE @jobName NVARCHAR(128) = N'AsyncCallJob_' + CAST(@ExecutionId AS NVARCHAR(10));

DECLARE @jobId UNIQUEIDENTIFIER;

EXEC msdb.dbo.sp_add_job
    @job_name = @jobName,
    @enabled = 1,
    @owner_login_name = N'sa',
    @delete_level = 1, -- Delete the job when it succeeds
    @job_id = @jobId OUTPUT;
    
-- 2. Add a job step
EXEC msdb.dbo.sp_add_jobstep
    @job_id = @jobId,
    @step_name = N'AsyncCallStep',
    @subsystem = N'TSQL',
    @database_name=N'AsyncTest',
    @command = @command;

-- 3. Add the job to the server
EXEC msdb.dbo.sp_add_jobserver @job_id = @jobId, @server_name = N'(local)'    

-- 4. Start the job
EXEC msdb.dbo.sp_start_job @job_name = @jobName;

-- 5. Update the main script finish time
UPDATE dbo.AsyncCallsLog
SET FinishMainScriptTime = SYSDATETIME()
WHERE ExecutionId = @ExecutionId
GO

Параметр @delete_level ставимо 1, щоб у разі помилки завдання не видалялось і можна було проаналізувати повідомлення про помилку.

Запустимо скрипт кілька разів, щоб зібрати якусь статистику.

І приклад скрипту для створення постійно існуючого завдання. Спочатку створимо джобу:

-- 1. Create the job
DECLARE @command NVARCHAR(MAX) = N'EXEC dbo.AsyncCallExecute @ExecutionId = 1';

DECLARE @jobName NVARCHAR(128) = N'AsyncCallJob';

DECLARE @jobId UNIQUEIDENTIFIER;

EXEC msdb.dbo.sp_add_job
    @job_name = @jobName,
    @enabled = 1,
    @owner_login_name = N'sa',
    @delete_level = 0,
    @job_id = @jobId OUTPUT;
    
-- 2. Add a job step
EXEC msdb.dbo.sp_add_jobstep
    @job_id = @jobId,
    @step_name = N'AsyncCallStep',
    @subsystem = N'TSQL',
    @database_name=N'AsyncTest',
    @command = @command;

-- 3. Add the job to the server
EXEC msdb.dbo.sp_add_jobserver @job_id = @jobId, @server_name = N'(local)'
GO

І для запуску з передачею параметра:

-- 0. Get the next ExecutionId
DECLARE @ExecutionId int

EXEC    [dbo].[AsyncCallStart]
        @AsyncMethod = 'SQL Server Agent (existing job)',
        @ExecutionId = @ExecutionId OUTPUT

SELECT  @ExecutionId as N'@ExecutionId'

-- 1. Update the job step command
DECLARE @command NVARCHAR(MAX) = N'EXEC dbo.AsyncCallExecute @ExecutionId = ' + CAST(@ExecutionId AS NVARCHAR(10))

EXEC msdb.dbo.sp_update_jobstep @job_name = 'AsyncCallJob', @step_id=1 , 
        @command=@command

-- 2. Start the job
EXEC msdb.dbo.sp_start_job @job_name = 'AsyncCallJob'

-- 3. Update the main script finish time
UPDATE dbo.AsyncCallsLog
SET FinishMainScriptTime = SYSDATETIME()
WHERE ExecutionId = @ExecutionId

Також виконаємо скрипт кілька разів.

SSIS Catalog

SSIS (SQL Server Integration Services) Catalog вперше з’явився в SQL Server 2012, до цього пакети зберігались як файли або в базі msdb.

В SSIS SQL код запускається через задачу Execute SQL Task, тобто для підготовки нам потрібно в новому або існуючому проєкті створити пакет з єдиною задачею Execute SQL Task і опублікувати цей пакет в проєкті SSIS Catalog.

Створимо в новому або існуючому SSIS проєкті пакет AsyncCallPackage.dtsx:

В параметри пакета додамо параметр ExecutionId, який буде передаватись в нашу процедуру:

Опублікуємо цей пакет на сервері в SSIS Catalog в складі існуючого або нового проєкта.

Тепер ми можемо запускати нашу процедуру асинхронно за допомогою такого коду:

-- 0. Get the next ExecutionId
DECLARE	@ExecutionId int

EXEC	[dbo].[AsyncCallStart]
		@AsyncMethod = 'SSIS Catalog',
		@ExecutionId = @ExecutionId OUTPUT

SELECT	@ExecutionId as N'@ExecutionId'

-- 1. Create the execution
DECLARE @execution_id bigint
EXEC [SSISDB].[catalog].[create_execution] @package_name=N'AsyncCallPackage.dtsx', @execution_id=@execution_id OUTPUT, 
	@folder_name=N'AsyncCall', @project_name=N'AsyncCall'

EXEC [SSISDB].[catalog].[set_execution_parameter_value] @execution_id,  @object_type=30, @parameter_name=N'ExecutionId', @parameter_value=@ExecutionId

-- 2. Start the execution
EXEC [SSISDB].[catalog].[start_execution] @execution_id

-- 3. Update the main script finish time
UPDATE dbo.AsyncCallsLog
SET FinishMainScriptTime = SYSDATETIME()
WHERE ExecutionId = @ExecutionId
GO

Також зробимо кілька запусків.

Можна помітити, що перший запуск проходить трішки довше, ніж наступні. Ймовірно, це пов’язано з очисткою кеша (JIT, плани виконання, дані в пам’яті, завантажені DLL) після довгого «простою».

Це наразі все щодо «рідних» засобів асинхронного запуску коду в Transact-SQL.
Першою думкою буде питання, а як щодо SQL CLR та sp_execute_external_script? Там використовуються мови високого рівня, які мають вбудовані засоби для асинхронного виконання.

SQL CLR

Щодо SQL CLR. Якщо, наприклад, опублікувати таку SQL CLR процедуру з PERMISSION_SET = SAFE,

using Microsoft.SqlServer.Server;
using System;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using System.Threading;

public partial class StoredProcedures
{
    [SqlProcedure]
    public static void AsyncCallThread(SqlInt32 ExecutionId)
    {

        string connStr = "context connection=true";
        
        Thread thread = new Thread(() =>
        {
            try
            {
                using (SqlConnection conn = new SqlConnection(connStr))
                {
                    conn.Open();
                    using (SqlCommand cmd = new SqlCommand($"EXEC dbo.AsyncCallExecute @ExecutionId = {ExecutionId.Value}", conn))
                    {
                        cmd.ExecuteNonQuery();
                    }
                }
            }
            catch (Exception ex)
            {
                SqlContext.Pipe.Send($"Error calling service: {ex.Message}");
            }
        });

        thread.IsBackground = true;
        thread.Start();
    }
}

то при її виклику

DECLARE @ExecutionId int

EXEC    [dbo].[AsyncCallStart]
        @AsyncMethod = 'SQL CLR Thread',
        @ExecutionId = @ExecutionId OUTPUT

SELECT  @ExecutionId as N'@ExecutionId'

EXEC [dbo].[AsyncCallThread] @ExecutionId = @ExecutionId
GO

ми отримаємо помилку:

Msg 6522, Level 16, State 1, Procedure dbo.AsyncCallThread, Line 0
A .NET Framework error occurred during execution of user-defined routine or aggregate "AsyncCallThread": 
System.Security.HostProtectionException: Attempted to perform an operation that was forbidden by the CLR host.
The protected resources (only available with full trust) were: All
The demanded resources were: SelfAffectingThreading
System.Security.HostProtectionException: 
   at StoredProcedures.AsyncCallThread(SqlInt32 ExecutionId)
.

У SQL Server CLR код виконується всередині спеціального «hosted CLR» середовища, і за замовчуванням заборонені певні речі, в тому числі створення нових потоків і асинхронне виконання в наборі дозволів SAFE та EXTERNAL_ACCESS.

Якщо ж ви спробуєте надати вашій збірці набір дозволів UNSAFE:

ALTER ASSEMBLY [AsyncCall] WITH PERMISSION_SET = UNSAFE

То запуск завершиться без помилок, але цільова процедура виконана не буде, потік піде в «нікуди»:

Але коли ми змінимо рядок з’єднання з «context connection=true» на звичайний рядок зовнішнього з’єднання типу «Data Source=<server>;Initial Catalog=<database>; Integrated Security=True», то все працює.

Причина в наступному. «context connection=true» — це спеціальний режим, коли CLR-код не відкриває «новий» TCP-конект, а користується тим самим внутрішнім з’єднанням, через яке викликається наша процедура. Усі команди в такому режимі виконуються синхронно в рамках того ж сесійного контексту. І, наприклад, якщо ми запускаємо новий потік і там теж користуємося «context connection=true», то ми намагаємось відкрити друге логічне з’єднання поверх того ж сесійного контексту, який уже «зайнятий» нашим CLR-викликом. SQL Server це не дозволяє — ми отримуємо винятки типу приведеного вище при наборі дозволів SAFE та EXTERNAL_ACCESS. А при UNSAFE SQL Server просто не дозволяє другому потоку зайти у той самий контекст, і виклик ігнорується.

У випадку зовнішнього з’єднання відкривається новий TCP-конект до SQL Server. Він працює як клієнтський. Провайдер SqlClient у цьому режимі повністю підтримує async/await. SQL CLR запускає новий потік, відкриває справжнє з’єднання до SQL Server і асинхронно виконує запит.

Ось приклад SQL CLR процедури з використанням async/await:

    [SqlProcedure]
    public static void AsyncCallAsyncAwait(SqlString connectionString, SqlInt32 ExecutionId)
    {
        _ = ExecuteAsync(connectionString.Value, ExecutionId.Value);
    }

    private static async Task ExecuteAsync(string connectionString, int executionId)
    {
        try
        {
            using (var conn = new SqlConnection(connectionString))
            {
                await conn.OpenAsync().ConfigureAwait(false);

                using (SqlCommand cmd = new SqlCommand($"EXEC dbo.AsyncCallExecute @ExecutionId = {executionId}", conn))
                {
                    await cmd.ExecuteNonQueryAsync().ConfigureAwait(false);
                }
            }
        }
        catch (Exception ex)
        {
            SqlContext.Pipe.Send($"Error async calling: {ex.Message}");
        }
    }

Приклад запуску:

-- 0. Get the next ExecutionId
DECLARE	@ExecutionId int
DECLARE @connectionString nvarchar(4000) = N'Data Source=' + @@SERVERNAME + N';Initial Catalog=' + DB_NAME() + N';Integrated Security=True'

EXEC	[dbo].[AsyncCallStart]
		@AsyncMethod = 'SQL CLR',
		@ExecutionId = @ExecutionId OUTPUT

SELECT	@ExecutionId as N'@ExecutionId'

-- 1. Start the async execution
EXEC [dbo].[AsyncCallAsyncAwait] @connectionString = @connectionString, @ExecutionId = @ExecutionId

-- 2. Update the main script finish time
UPDATE dbo.AsyncCallsLog
SET FinishMainScriptTime = SYSDATETIME()
WHERE ExecutionId = @ExecutionId
GO 

Є ще варіант спробувати асинхронне виконання через sp_execute_external_script, але тут нас чекає невдача.

Всередині sp_execute_external_script код запускається не як звичайний процес, а під SQL Server Launchpad, у sandbox-оточенні.

Наприклад, для Python ми можемо використати асинхронні бібліотеки asyncio або aioodbc, але реально асинхронного ефекту з точки зору SQL Server ми не отримаємо, бо, по-перше, sp_execute_external_script завжди блокує виконання, поки Python-код не завершиться; по-друге, навіть якщо ви запустите в Python asyncio.create_task(...), по виходу з sp_execute_external_script процес Python буде знищений і задачі обірвуться.

Тобто asyncio можна використати для паралельної обробки усередині самого скрипта (наприклад, ходити одночасно у кілька джерел даних), але запустити процедуру у SQL Server «у фоні» і відпустити через sp_execute_external_script — ні, бо канал із SQL Server повинен отримати відповідь, перш ніж закрити сесію.

Але, раз ми уже дійшли до цього, то залишимо приклад коду для асинхронного запуску SQL запиту всередині sp_execute_external_script:

-- 0. Get the next ExecutionId
DECLARE @ExecutionId int
EXEC    [dbo].[AsyncCallStart]
        @AsyncMethod = 'External Script',
        @ExecutionId = @ExecutionId OUTPUT

SELECT  @ExecutionId as N'@ExecutionId'

-- 1. Start the external script
EXEC sp_execute_external_script
    @language = N'Python',
    @script = N'
import pyodbc
import asyncio

server = "..."
database = "..."
username = "..."
password = "..."
conn_str = f"DRIVER={{ODBC Driver 17 for SQL Server}};Server={server};Database={database};UID={username};PWD={password};"
sql = f"EXEC dbo.AsyncCallExecute @ExecutionId = {ExecutionId}"

def run_proc(conn_str, sql):
    with pyodbc.connect(conn_str, autocommit = True) as conn:
        with conn.cursor() as cursor:
            cursor.execute(sql)

async def run_proc_async(conn_str, sql):
    loop = asyncio.get_running_loop()
    await loop.run_in_executor(None, run_proc, conn_str, sql)

asyncio.run(run_proc_async(conn_str, sql))
',
    @params = N'@ExecutionId int',
    @ExecutionId = @ExecutionId;

Давайте спробуємо проаналізувати результати виконання наших скриптів.

Думаю, з назв і даних колонок зрозуміло їх зміст. Про всяк випадок додам пояснення щодо колонок з часом.

StartMainScriptTime — час старту основного скрипта, який запускає асинхронний виклик.
FinishMainScriptTime — час закінчення основного скрипта.

FinishAsyncCallTime — час закінчення виконання коду, який був запущений асинхронно. [MainScriptDuration, ms] — тривалість виконання основного скрипта в мілісекундах. [TotalDuration, ms] — проміжок часу від старту основного скрипта до закінчення виконання асинхронної частини.

[AsyncEngineDuration, ms] — умовно-приблизний час виконання асинхронного виклику саме частинами SQL Server, через які реалізовано асинхронність, дорівнює [TotalDuration, ms] — [AsyncEngineDuration, ms] — 10000 ms (пауза 10 секунд в процедурі dbo.AsyncCallExecute), дозволяє відносно оцінити швидкість відпрацювання механізму.

Які можна зробити попередні висновки?

  1. «Старт» кожного «механізму» йде довше, ніж наступні виклики. Напевно, для цього є і загальні, і окремі причини, властиві саме цьому механізму.
  2. Обробка черги Service Broker в «асинхронній» частині йде з затримками, можливо, через взаємні блокування, що потребує додаткового дослідження. Проте основний скрипт найшвидший серед усіх.
  3. Очікувано, що виконання через SQL Server Agent та SSIS Catalog проходить довше через більшу кількість задіяних ресурсів і складнішу внутрішню реалізацію, а через SQL CLR швидше, бо виконується в середовищі Common Language Runtime.
  4. Ну, і видно, що sp_execute_external_script не завершується, поки не закінчиться виконання всього зовнішнього скрипта, включно з асинхронним запуском.

І спробую дати порівняння описаних методів і навести сценарії, де їх можна застосовувати.

Механізм

Простота реалізації

Швидкодія

Недоліки

Типові сценарії

Service Broker

Середня: потрібна підготовка (контракти, черги, служби, процедура активації).

Висока в основному скрипті, низька в «асинхронній частині», де обробка черги може мати затримки.

Складна конфігурація.

Внутрішня асинхронна обробка подій, черги повідомлень між БД.

SQL Server Agent
(new job)

Середня: створення job динамічно, автоматичне видалення після виконання.

Середня.

Якщо завдання не видалилось — накопичення «сміття» в SQL Server Agent.

Запуск «важких» процедур, коли треба логувати й контролювати їх виконання.

SQL Server Agent (existing job)

Легка: достатньо оновлювати команду й стартувати job.

Середня.

Неможливо паралельно запустити один і той же job.

Регулярні завдання (backup, ETL-процеси).

SSIS Catalog

Складна: потрібен проєкт SSIS, пакет, публікація в SSIS Catalog.

Низька.

Вимагає наявності SSIS Catalog і налаштованого середовища.

Складні ETL-сценарії, коли асинхронність — лише додатковий бонус.

SQL CLR

Середня: потрібна збірка, налаштування прав (UNSAFE).

Висока: CLR виконується у середовищі .NET.

Обмеження безпеки, робота з потоками може бути нестабільна.

Коли треба максимально швидке виконання «фонових» завдань без зовнішніх інструментів.

Наразі все. Буду вдячний за коментарі й зауваження та виправлення помилок! Дякую за увагу!

Сподобалась стаття автора? Підписуйтесь на його акаунт вгорі сторінки, щоб отримувати сповіщення про нові публікації на пошту.

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

👍ПодобаєтьсяСподобалось6
До обраногоВ обраному4
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
інколи виникає потреба запустити якийсь код асинхронно з-під SQL

— что-то меня в последнее время Гондурас беспокоит ©

— что-то меня в последнее время Гондурас беспокоит ©

Обычный себе батч процессинг
приложение наваливает тасков, которые потом как нить разгребаются

ну є дофіга готових рішень для організації workflow/tasks — Netflix Conductor, Camunda, Temporal і т.д. і т.п.
Хтось навіть не полінився створити репо із їх списком: github.com/...​/awesome-workflow-engines
p.s.
Це я про те, що переважно починається з того що «нам всі ці фічі не потрібні» і робиться «просте рішення», яке в результаті натикається на одну чи кілька проблем які треба вирішувати, додаючи все нові і нові фічі/фікси в це «просте рішення».

ну є дофіга готових рішень для організації workflow/tasks — Netflix Conductor, Camunda, Temporal і т.д. і т.п.

рад за них
вот чего мне не хватало в моей системе — это еще одной системы, которую надо менеджить и за которую нужно платить деньги

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

а що таке

штатными средствами

табличка в базі?

штатніми средствами запуска скриптов в склй сервер является sql server agent или, как коллега привел в примере — встроенніе очереди
и хотя я довольно скептически отношусь к такому решению — я бы рассмотрел его для простых случаев, почему бы и нет?

зачем же так архитекторов обижать?

Не бог весть что, но сойдет
добавить оглавление со списком вариантов асинхронности, в разделы добавить ссылки на документацию — и будет вполне пригодное «ориентировочное» чтиво.

Підписатись на коментарі