Динамическая база данных хранимая процедура на SQL Server 2016 - PullRequest
0 голосов
/ 07 октября 2019

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

Например:

SP_Users принимает список @DATABASES в качестве параметров.

Для каждой базы данных необходимо выполнить один и тот же запрос и объединить результаты вместе.

Я считаю, что CTE может быть моей лучшей ставкой, поэтому у меня сейчас что-то подобное.

SET @DATABASES = 'DB_1, DB_2'  -- Two databases in a string listed

-- I have a split string function that will extract each database
SET @CURRENT_DB = 'DB_1'

WITH UsersCTE (Name, Email)
AS (SELECT Name, Email 
    FROM [@CURRENT_DB].[dbo].Users),

    SELECT @DATABASE as DB, Name, Email
    FROM UsersCTE

Чего я не хочу делать, так это жестко кодировать базы данных взапрос. Шаги, которые я изобразил:

  1. Разделите параметр @DATABASES, чтобы извлечь и установить @CURRENT_DB Переменная
  2. Повторять запрос с помощью рекурсивного CTE, пока все @DATABASESбыли обработаны
  3. Объедините все результаты вместе и верните данные.

Не уверен, что это правильный подход к решению этой проблемы.

Ответы [ 2 ]

1 голос
/ 07 октября 2019

Использование @databases:

Как уже упоминалось в комментариях к вашему вопросу, переменные не могут использоваться для динамического выбора базы данных. Динамический sql указывается. Вы можете начать с создания вашего шаблона SQL-оператора:

declare @sql nvarchar(max) = 
    'union all ' + 
    'select ''@db'' as db, name, email ' + 
    'from [@db].dbo.users ';

Поскольку у вас есть SQL Server 2016, вы можете разделить, используя функцию string_split, с вашей переменной @databases в качестве входных данных. Это приведет к появлению таблицы со значением в качестве имени столбца, в которой хранятся имена базы данных.

Используйте функцию replace для замены @db в шаблоне на value. Это приведет к одному выражению sql для каждой базы данных, которую вы передали в @databases. Затем объедините утверждения обратно вместе. К сожалению, в версии 2016 нет встроенной функции для этого. Таким образом, мы должны использовать знаменитый трюк for xml, чтобы соединить операторы, затем мы используем .value, чтобы преобразовать его в строку, и, наконец, мы используем stuff, чтобы избавиться от ведущего оператора union all.

Возьмите результаты каскадного вывода и перезапишите переменную @sql. На этом этапе все готово, поэтому выполните его.

Я делаю все, что описано в этом коде:

declare @databases nvarchar(max) = 'db_1,db_2';

set @sql = stuff(
    (

        select      replace(@sql, '@db', value)
        from        string_split(@databases, ',')
        for xml     path(''), type

    ).value('.[1]', 'nvarchar(max)')
    , 1, 9, '');

exec(@sql);

Без проверки, конечно, но если вы печатаете вместо выполненияПохоже, что это дает правильный SQL-оператор для ваших нужд.

Использование msForEachDB:

Теперь, если вы не хотите знать, какие базы данных имеют «пользователей», например, есливы находитесь в среде, где у вас есть разные базы данных для каждого клиента, вы можете использовать sp_msForEachDb и сначала проверить структуру, чтобы убедиться, что в ней есть таблица 'users' со столбцами 'name' и 'email'. Если это так, выполните соответствующий оператор. Если нет, выполнить фиктивный оператор. Я не буду описывать это, я просто дам код:

declare @aggregator table (
    db sysname,
    name int,
    email nvarchar(255)
);

insert @aggregator
exec sp_msforeachdb '

    declare @sql nvarchar(max) = ''select db = '''''''', name = '''''''', email = '''''''' where 1 = 2'';

    select      @sql = ''select db = ''''?'''', name, email from ['' + table_catalog + ''].dbo.users''
    from        [?].information_schema.columns
    where       table_schema = ''dbo''
    and         table_name = ''users''
    and         column_name in (''name'', ''email'')
    group by    table_catalog
    having      count(*) = 2

    exec (@sql);

';


select      *
from        @aggregator
0 голосов
/ 08 октября 2019

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

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

CREATE PROCEDURE [dbo].[SP_Users](
    @DATABASES VARCHAR(MAX) = NULL,
    @PARAM1 VARCHAR(250),
    @PARAM2 VARCHAR(250)
)

BEGIN 
    SET NOCOUNT ON;

    --Local variables
    DECLARE 
     @COUNTER INT = 0,
     @SQL NVARCHAR(MAX) = '',
     @CURRENTDB VARCHAR(50) = NULL,
     @MAX INT = 0,
     @ERRORMSG VARCHAR(MAX)

    --Check we have databases entered
    IF @DATABASES IS NULL 
    BEGIN
        RAISERROR('ERROR: No Databases Provided, 
        Please Provide a list of databases to execute procedure. See stored procedure: 
        [SP_Users]', 16, 1) 
        RETURN 
    END

    -- SET Number of iterations based on number of returned databases
    SET @MAX = (SELECT COUNT(*) FROM 
               (SELECT ROW_NUMBER() OVER (ORDER BY i.value) AS RowNumber,  i.value 
               FROM dbo.udf_SplitVariable(@DATABASES, ',') AS i)X)

    -- Build SQL Statement 
    WHILE @COUNTER < @MAX
        BEGIN

        --Set the current database
        SET @CURRENTDB = (SELECT X.Value FROM 
            (SELECT ROW_NUMBER() OVER (ORDER BY i.value) AS RowNumber,  i.value 
            FROM dbo.udf_SplitVariable(@DATABASES, ',') AS i
            ORDER BY RowNumber OFFSET @COUNTER
            ROWS FETCH NEXT 1 ROWS ONLY) X);


        SET @SQL = @SQL + N'
                          (
                          SELECT Name, Email
                          FROM [' + @CURRENTDB + '].[dbo].Users
                          WHERE 
                              (Name = @PARAM1 OR @PARAM1 IS NULL)
                              (Email = @PARAM2 OR @PARAM2 IS NULL)
                          ) '
                          + N' UNION ALL '

            END
            PRINT @CURRENTDB
            PRINT @SQL

            SET @COUNTER = @COUNTER + 1
        END

        -- remove last N' UNION ALL '
        IF LEN(@SQL) > 11
            SET @SQL = LEFT(@SQL, LEN(@SQL) - 11)

        EXEC sp_executesql @SQL, N'@CURRENTDB VARCHAR(50), 
                                   @PARAM1 VARCHAR(250), 
                                   @PARAM2 VARCHAR(250)', 
                                   @CURRENTDB, 
                                   @PARAM1 , 
                                   @PARAM2


END

Функция разделения переменных

CREATE FUNCTION [dbo].[udf_SplitVariable]
(
    @List varchar(8000),
    @SplitOn varchar(5) = ','
)

RETURNS @RtnValue TABLE
(
    Id INT IDENTITY(1,1),
    Value VARCHAR(8000)
)

AS
BEGIN

--Account for ticks
SET @List = (REPLACE(@List, '''', ''))

--Account for 'emptynull'
IF LTRIM(RTRIM(@List)) = 'emptynull'
BEGIN
    SET @List = ''
END

--Loop through all of the items in the string and add records for each item
WHILE (CHARINDEX(@SplitOn,@List)>0)
BEGIN

    INSERT INTO @RtnValue (value)
    SELECT Value = LTRIM(RTRIM(SUBSTRING(@List, 1, CHARINDEX(@SplitOn, @List)-1)))  

    SET @List = SUBSTRING(@List, CHARINDEX(@SplitOn,@List) + LEN(@SplitOn), LEN(@List))

END

INSERT INTO @RtnValue (Value)
SELECT Value = LTRIM(RTRIM(@List))

RETURN

END 
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...