【问题标题】:Writing stored procedures when using dynamic schema names in sql server在 sql server 中使用动态模式名​​称时编写存储过程
【发布时间】:2019-08-19 11:19:22
【问题描述】:

我目前使用的应用程序的表具有不同的模式名称,例如 Table1 可以有多个存在,例如 A.Table1 和 B.Table1。我所有的存储过程都存储在 dbo 下。我正在使用动态 SQL 编写以下存储过程。我目前使用的是 SQL Server 2008 R2,很快就会迁移到 SQL Server 2012。

create procedure dbo.usp_GetDataFromTable1
@schemaname varchar(100),
@userid bigint
as
begin
    declare @sql nvarchar(4000)
    set @sql='select a.EmailID from '+@schemaname+'.Table1 a where a.ID=@user_id';
    exec sp_executesql @sql, N'@user_id bigint', @user_id=@userid
end

现在我的问题是, 1. 这种方法会影响我的存储过程的性能吗? 2. 如果性能受到影响,那么这种场景如何编写程序?

【问题讨论】:

  • 也许您没有参与设计,但这听起来像是 2 个数据库的绝佳方案。就个人而言,我认为这是一个糟糕的想法。当您需要连接两个表时会发生什么?索引和所有优化技巧都消失了。
  • @Jeremy 是的,你是对的。我不参与设计。

标签: sql-server stored-procedures


【解决方案1】:

如果可能的话,最好的解决办法是重新设计。

您甚至可以通过添加一个新列来替换架构来追溯实现这一点,例如:Profile,然后将每个架构中的所有表合并到一个架构中(例如dbo)。

那么您的程序将如下所示:

create procedure dbo.usp_GetDataFromTable1
@profile int,
@userid bigint
as
begin
    select a.EmailID from dbo.Table1 a 
    where a.ID = @user_id
    and a.Profile = @profile
end

我在 profile 列中使用了 int,但如果您使用 varchar,您甚至可以为 profile 值保留架构名称,如果这有助于使事情更清晰。

【讨论】:

    【解决方案2】:

    我将研究一种供应方法,您可以在其中动态创建表和存储过程,作为一些前期过程的一部分。我不能 100% 确定您的情况,但也许这可能是您添加新用户时。然后,您可以在应用程序中按照约定调用这些 SP。

    例如,新用户创建调用创建 c.Table 和 c.GetDetails SP 的 SP。

    然后在应用程序中,您可以根据“c”作为用户定义的属性调用 c.GetDetails。

    这可以帮助您解决使用动态 SQL 的任何安全问题。它仍然是动态的,但是是预先构建的。

    【讨论】:

    • 是的,这可以做到,但是如果我需要更改 SP 会怎样?已经创建了吗?
    • 这是一个挑战。您要么需要编写一种方法来更新过程(可能从应用程序内部)并跟踪架构版本,要么让所有过程调用单个参数化过程。
    • 感谢您的建议...我会试一试的。
    【解决方案3】:

    动态模式和相同的表结构很不寻常,但您仍然可以使用以下方式获得所需的内容:

    declare @sql nvarchar(4000)
    declare @schemaName VARCHAR(20) = 'schema'
    declare @tableName VARCHAR(20) = 'Table'
    -- this will fail, as the whole string will be 'quoted' within [..]
    -- declare @tableName VARCHAR(20) = 'Category; DELETE FROM TABLE x;'
    
    set @sql='select * from ' + QUOTENAME(@schemaName) + '.' + QUOTENAME(@tableName)
    PRINT @sql
    
    -- @user_id is not used here, but it can if the query needs it
    exec sp_executesql @sql, N'@user_id bigint', @user_id=0
    

    所以,QUOTENAME 在 SQL 注入方面应该保持安全。

    1.性能 - 动态 SQL 无法从某些性能改进中受益(我认为过程相关的统计信息或类似的东西),因此存在性能风险。

    但是,对于在相当少量数据(最多数千万)上运行的简单事情以及对于变化不大的数据(插入和删除),我认为您不会有明显的问题。

    2。替代 -bukko 提出了解决方案。由于所有表具有相同的结构,因此可以合并它们。如果它变得很大,良好的索引和分区应该能够减少查询执行时间。

    【讨论】:

      【解决方案4】:

      如果您知道要使用的架构,则可以解决此问题。您在此处声明架构名称是在注册时创建的,我们在登录时使用此方法。我有一个视图,可以在会话启动/处置时添加或删除联合。下面的例子。

      CREATE VIEW [engine].[vw_Preferences]
      AS
      SELECT TOP (0) CAST (NULL AS NVARCHAR (255)) AS SessionID,
                     CAST (NULL AS UNIQUEIDENTIFIER) AS [PreferenceGUID],
                     CAST (NULL AS NVARCHAR (MAX)) AS [Value]
      UNION ALL SELECT 'ZZZ_7756404F411B46138371B45FB3EA6ADB', * FROM ZZZ_7756404F411B46138371B45FB3EA6ADB.Preferences
      UNION ALL SELECT 'ZZZ_CE67D221C4634DC39664975494DB53B2', * FROM ZZZ_CE67D221C4634DC39664975494DB53B2.Preferences
      UNION ALL SELECT 'ZZZ_5D6FB09228D941AC9ECD6C7AC47F6779', * FROM ZZZ_5D6FB09228D941AC9ECD6C7AC47F6779.Preferences
      UNION ALL SELECT 'ZZZ_5F76B619894243EB919B87A1E4408D0C', * FROM ZZZ_5F76B619894243EB919B87A1E4408D0C.Preferences
      UNION ALL SELECT 'ZZZ_A7C5ED1CFBC843E9AD72281702FCC2B4', * FROM ZZZ_A7C5ED1CFBC843E9AD72281702FCC2B4.Preferences
      

      第一个选择前 0 行是一个后备,所以我总是有一个默认定义和一个静态表定义。您可以从视图中选择并通过会话 ID 过滤

      SELECT  PreferenceGUID, Value
        FROM  engine.vw_Preferences
       WHERE  SessionID = 'ZZZ_5D6FB09228D941AC9ECD6C7AC47F6779';
      

      这里有趣的部分是当视图中有静态值时如何生成执行计划。不会产生结果的联合不会被代码评估,留下一个没有任何连接或联合的基本执行计划......

      您可以对此进行测试,它与直接从表中读取一样有效(误差范围很小,没有人会关心)。甚至可以通过使用“替代”触发器来替换回写过程,然后在后台构建动态 sql。动态 sql 在写入方面效率较低,但这意味着您可以通过视图更新任何表,通常只能使用单个表视图。

      【讨论】:

        【解决方案5】:

        Dynamic Sql 通常会影响性能和安全性,大多数情况下是最坏的。但是,由于您不能参数化标识符,这可能是您唯一的方法,除非您愿意为每个模式复制存储过程:

        create procedure dbo.usp_GetDataFromTable1
        @schemaname varchar(100),
        @userid bigint
        as
        begin
            if @schemaname = 'a' 
            begin
                select EmailID from a.Table1 where ID = @user_id
            end
            else if schemaname = 'b' 
            begin
                select EmailID from b.Table1 where ID = @user_id
            end
        
        end
        

        【讨论】:

        • 我不能使用这个,因为模式名 a 是在用户注册时动态创建的。
        【解决方案6】:

        我能想到这样做的唯一原因是满足多个租户。你很接近,但你采取的方法是错误的。

        据我所知,多租户有 3 种解决方案:每个租户的数据库、每个租户的单一数据库架构或单一数据库单一架构(又名,逐行租户)。

        这里的其他用户已经提到了其中两个。尚未真正详细说明的是每个租户的架构,这就是您所属于的样子。对于这种方法,您需要更改查看数据库的方式。此时的数据库只是模式的容器。每个模式都可以有自己的设计、存储过程、触发器、队列、函数等。主要目标是数据隔离。您不希望租户 A 看到租户 B 的东西。每个租户模式方法的优点是您可以更灵活地更改租户特定的数据库。它还允许您比每个租户的数据库方法更容易扩展。

        回答:您应该为每个模式创建相同的存储过程(创建过程示例:schema_name.stored_proc_name),而不是编写动态 SQL 来考虑使用 DBO 用户的模式。为了运行模式的存储过程,您需要模拟与相关模式相关的用户。它看起来像这样:

        execute as user = 'tenantA'
        exec sp_testing
        revert --revert will take us back to the original user, most likely DBO in your case.
        

        跨所有租户的数据整理有点困难。我知道的唯一解决方案是使用 DBO 用户运行并分别在所有模式中“合并”所有结果,如果你有大量模式,这有点乏味。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2017-01-26
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-08-16
          • 1970-01-01
          • 2017-08-22
          相关资源
          最近更新 更多