ID primero: este es el campo más selectivo (es decir, el más exclusivo). Pero al ser un campo de incremento automático (o aleatorio si todavía se usan GUID), los datos de cada cliente se distribuyen en cada tabla. Esto significa que hay momentos en que un cliente necesita 100 filas, y eso requiere que se lean casi 100 páginas de datos del disco (no rápido) en el Grupo de búferes (ocupando más espacio que 10 páginas de datos). También aumenta la contención en las páginas de datos, ya que será más frecuente que múltiples clientes necesiten actualizar la misma página de datos.
Sin embargo, por lo general, no se topa con tantos problemas de detección de parámetros / plan de caché incorrecto, ya que las estadísticas en los diferentes valores de ID son bastante consistentes. Es posible que no obtenga los planes más óptimos, pero será menos probable que obtenga planes horribles. Este método esencialmente sacrifica el rendimiento (ligeramente) en todos los clientes para obtener el beneficio de problemas menos frecuentes.
TenantID primero:Esto no es selectivo en absoluto. Puede haber muy poca variación en 1 millón de filas si solo tiene 100 TenantID. Pero las estadísticas para estas consultas son más precisas ya que SQL Server sabrá que una consulta para el Inquilino A retirará 500,000 filas, pero esa misma consulta para el Inquilino B es de solo 50 filas. Aquí es donde está el principal punto de dolor. Este método aumenta en gran medida las posibilidades de tener problemas de detección de parámetros donde la primera ejecución de un Procedimiento almacenado es para el Inquilino A y actúa de manera adecuada en función de que el Optimizador de consultas vea esas estadísticas y sepa que debe ser eficiente para obtener 500k filas. Pero cuando el Inquilino B, con solo 50 filas, se ejecuta, ese plan de ejecución ya no es apropiado y, de hecho, es bastante inapropiado. Y, dado que los datos no se insertan en el orden del campo inicial,
Sin embargo, para que el primer TenantID ejecute un Procedimiento almacenado, el rendimiento debería ser mejor que en el otro enfoque, ya que los datos (al menos después de realizar el mantenimiento del índice) se organizarán física y lógicamente de modo que se necesiten muchas menos páginas de datos para satisfacer el consultas Esto significa menos E / S físicas, menos lecturas lógicas, menos contención entre Inquilinos por las mismas páginas de datos, menos espacio desperdiciado ocupado en el Grupo de búferes (por lo tanto, mejora la esperanza de vida de la página), etc.
Hay dos costos principales para obtener este rendimiento mejorado. El primero no es tan difícil: debe realizar un mantenimiento de índice regular para contrarrestar el aumento de la fragmentación. El segundo es un poco menos divertido.
Para contrarrestar los problemas de detección de parámetros aumentados, debe separar los planes de ejecución entre los inquilinos. El enfoque simplista es usar WITH RECOMPILE
en procs o la OPTION (RECOMPILE)
sugerencia de consulta, pero eso es un éxito en el rendimiento que podría borrar todas las ganancias obtenidas al poner TenantID
primero. El método que encontré que funcionó mejor es usar SQL dinámico parametrizado a través de sp_executesql
. La razón por la que se necesita el SQL dinámico es permitir la concatenación del TenantID en el texto de la consulta, mientras que todos los demás predicados que normalmente serían parámetros siguen siendo parámetros. Por ejemplo, si estaba buscando un Pedido en particular, haría algo como:
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;
El efecto que esto tiene es crear un plan de consulta reutilizable para ese TenantID que coincida con el volumen de datos de ese Inquilino en particular. Si ese mismo Inquilino A ejecuta el procedimiento almacenado nuevamente para otro @OrderID
, reutilizará ese plan de consulta en caché. Un inquilino diferente que ejecute el mismo procedimiento almacenado generaría un texto de consulta que solo era diferente en el valor del TenantID, pero cualquier diferencia en el texto de consulta es suficiente para generar un plan diferente. Y el plan generado para el Inquilino B no solo coincidirá con el volumen de datos para el Inquilino B, sino que también será reutilizable para el Inquilino B para diferentes valores de @OrderID
(ya que ese predicado todavía está parametrizado).
Las desventajas de este enfoque son:
- Es un poco más de trabajo que simplemente escribir una consulta simple (pero no todas las consultas deben ser SQL dinámico, solo las que terminan teniendo el problema de detección de parámetros).
- Dependiendo de cuántos inquilinos hay en un sistema, aumenta el tamaño de la caché del plan ya que cada consulta ahora requiere 1 plan por TenantID que lo está llamando. Esto podría no ser un problema, pero al menos es algo a tener en cuenta.
El SQL dinámico rompe la cadena de propiedad, lo que significa que no se puede asumir el acceso de lectura / escritura a las tablas al tener EXECUTE
permiso en el Procedimiento almacenado. La solución fácil pero menos segura es simplemente dar al usuario acceso directo a las tablas. Ciertamente, esto no es lo ideal, pero suele ser una solución de compromiso rápida y fácil. El enfoque más seguro es utilizar la seguridad basada en certificados. Es decir, crear un Certificado, luego crear un Usuario a partir de ese Certificado, otorgarle a ese Usuario los permisos deseados (un Usuario o Inicio de Sesión basado en un Certificado no puede conectarse a SQL Server por sí solo) y luego firmar los Procedimientos Almacenados que usan SQL Dinámico con eso mismo certificado a través de AGREGAR FIRMA .
Para obtener más información sobre la firma de módulos y certificados, consulte: ModuleSigning.Info
(ID, TenantID)
y también crea un índice no agrupado(TenantID, ID)
, o simplemente(TenantID)
para tener estadísticas precisas de consultas que procesan la mayoría de las filas de un solo inquilino?