先租户ID:这根本不是选择性的。如果只有100个TenantID,则100万行的变化可能很小。但是,由于SQL Server知道对租户A的查询将拉回500,000行,而对租户B的相同查询只有50行,因此这些查询的统计信息更加准确。这是主要的痛点所在。如果存储过程的第一次运行是针对租户A的,则此方法极大地增加了出现参数嗅探问题的机会,并且基于查询优化器看到这些统计信息并知道需要有效地获取50万行,从而适当地采取了行动。但是,当只有50行的Tenant B运行时,该执行计划不再适用,实际上是不合适的。并且,由于未按前导字段的顺序插入数据,
但是,对于第一个运行存储过程的TenantID,性能应优于其他方法,因为数据(至少在进行索引维护之后)将在物理和逻辑上进行组织,从而需要更少的数据页来满足查询。这意味着更少的物理I / O,更少的逻辑读取,更少的相同数据页面的租户之间的争用,更少的缓冲池占用的空间(因此提高了页面寿命)。
获得此改进的性能有两个主要成本。第一个并不是那么困难:您必须定期维护索引以抵消增加的碎片。第二个不太有趣。
为了解决增加的参数嗅探问题,您需要在租户之间分离执行计划。简单的方法是WITH RECOMPILE
在proc或OPTION (RECOMPILE)
查询提示上使用,但这对性能造成了打击,可能会抹杀所有因获得TenantID
第一而获得的收益。我发现效果最好的方法是通过使用参数化Dynamic SQL sp_executesql
。需要使用动态SQL的原因是允许将TenantID连接到查询的文本中,而通常用作参数的所有其他谓词仍然是参数。例如,如果您要查找特定的订单,则可以执行以下操作:
DECLARE @GetOrderSQL NVARCHAR(MAX);
SET @GetOrderSQL = N'
SELECT ord.field1, ord.field2, etc.
FROM dbo.Orders ord
WHERE ord.TenantID = ' + CONVERT(NVARCHAR(10), @TenantID) + N'
AND ord.OrderID = @OrderID_dyn;
';
EXEC sp_executesql
@GetOrderSQL,
N'@OrderID_dyn INT',
@OrderID_dyn = @OrderID;
这样做的效果是仅为该TenantID创建一个可重用的查询计划,该计划将匹配该特定Tenant的数据量。如果同一租户A再次执行该存储过程,@OrderID
则它将重用该缓存的查询计划。运行同一存储过程的另一个租户将生成一个查询文本,该查询文本仅在TenantID的值上有所不同,但是查询文本中的任何差异足以生成一个不同的计划。而且,为租户B生成的计划不仅将与租户B的数据量匹配,而且对于不同的值@OrderID
(由于仍对谓词进行了参数化),它还可重用于租户B。
这种方法的缺点是:
- 与仅键入一个简单查询相比,这需要做更多的工作(但并非所有查询都必须是Dynamic SQL,只是那些最终会出现参数嗅探问题的查询)。
- 根据系统上有多少租户,它确实会增加计划缓存的大小,因为每个查询现在每个调用它的TenantID都需要1个计划。这可能不是问题,但至少要注意一点。
动态SQL破坏了所有权链,这意味着无法通过EXECUTE
对存储过程的许可来假定对表的读/写访问。简单但安全性较低的修复方法只是让用户直接访问表。这当然不是理想的,但这通常是快速简便的权衡。更为安全的方法是使用基于证书的安全性。意思是,创建一个证书,然后从该证书创建一个用户,向该用户授予所需的权限(基于证书的用户或登录名不能自行连接到SQL Server),然后使用该协议对使用动态SQL的存储过程进行签名通过添加签名获得相同的证书。
有关模块签名和证书的更多信息,请参见:ModuleSigning.Info