ID primeiro: este é o campo mais seletivo (ou seja, o mais exclusivo). Porém, por ser um campo de incremento automático (ou aleatório se ainda estiver usando GUIDs), os dados de cada cliente são espalhados por cada tabela. Isso significa que há momentos em que um cliente precisa de 100 linhas e que exige quase 100 páginas de dados lidas do disco (não rápido) no Buffer Pool (ocupando mais espaço que 10 páginas de dados). Também aumenta a contenção nas páginas de dados, pois será mais frequente que vários clientes precisem atualizar a mesma página de dados.
No entanto, normalmente você não encontra tantos problemas de sniffing de parâmetro / plano de cache ruim, pois as estatísticas entre os diferentes valores de ID são razoavelmente consistentes. Você pode não ter os melhores planos, mas é menos provável que tenha planos horríveis. Esse método sacrifica essencialmente o desempenho (ligeiramente) em todos os clientes para obter o benefício de problemas menos frequentes.
TenantID primeiro:Isso não é muito seletivo. Pode haver muito pouca variação em 1 milhão de linhas se você tiver apenas 100 TenantIDs. Mas as estatísticas para essas consultas são mais precisas, pois o SQL Server saberá que uma consulta para o inquilino A recuará 500.000 linhas, mas a mesma consulta para o inquilino B é de apenas 50 linhas. É aqui que está o principal ponto de dor. Esse método aumenta muito as chances de ocorrer problemas de detecção de parâmetros nos casos em que a primeira execução de um Procedimento armazenado é para o inquilino A e atua adequadamente com base no Query Optimizer, visualizando essas estatísticas e sabendo que precisa ser eficiente para obter 500 mil linhas. Mas quando o inquilino B, com apenas 50 linhas, é executado, esse plano de execução não é mais apropriado e, de fato, é bastante inadequado. E, como os dados não estão sendo inseridos na ordem do campo principal,
No entanto, para o primeiro TenantID executar um Procedimento Armazenado, o desempenho deve ser melhor do que na outra abordagem, pois os dados (pelo menos depois de fazer a manutenção do índice) serão física e logicamente organizados de forma que sejam necessárias muito menos páginas de dados para satisfazer o consultas. Isso significa menos E / S física, menos leituras lógicas, menos disputas entre os inquilinos pelas mesmas páginas de dados, menos espaço desperdiçado ocupado no Buffer Pool (daí a expectativa de vida útil da página melhorada) etc.
Existem dois custos principais para obter esse desempenho aprimorado. O primeiro não é tão difícil: você deve fazer a manutenção regular do índice para neutralizar o aumento da fragmentação. O segundo é um pouco menos divertido.
Para combater o aumento dos problemas de detecção de parâmetros, é necessário separar os planos de execução entre os inquilinos. A abordagem simplista é usar WITH RECOMPILE
em procs ou a OPTION (RECOMPILE)
dica de consulta, mas isso é um impacto no desempenho que pode acabar com todos os ganhos obtidos com a colocação em TenantID
primeiro lugar. O método que eu achei que funcionou melhor é usar o SQL dinâmico parametrizado via sp_executesql
. O motivo para a necessidade do SQL dinâmico é permitir concatenar o TenantID no texto da consulta, enquanto todos os outros predicados que normalmente seriam parâmetros ainda são parâmetros. Por exemplo, se você estivesse procurando um pedido específico, faria 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;
O efeito disso é criar um plano de consulta reutilizável apenas para esse TenantID que corresponda ao volume de dados desse inquilino específico. Se o mesmo inquilino A executar o procedimento armazenado novamente para outro @OrderID
, ele reutilizará esse plano de consulta em cache. Um inquilino diferente executando o mesmo procedimento armazenado geraria um texto de consulta diferente apenas no valor do TenantID, mas qualquer diferença no texto da consulta é suficiente para gerar um plano diferente. E o plano gerado para o inquilino B não corresponderá apenas ao volume de dados do inquilino B, mas também será reutilizável para o inquilino B para diferentes valores de @OrderID
(já que esse predicado ainda está parametrizado).
As desvantagens dessa abordagem são:
- É um pouco mais trabalhoso do que apenas digitar uma consulta simples (mas nem todas as consultas precisam ser de SQL Dinâmico, apenas aquelas que acabam tendo o problema de cheirar o parâmetro).
- Dependendo de quantos inquilinos estão em um sistema, ele aumenta o tamanho do cache do plano, já que cada consulta agora exige 1 plano por TenantID que está chamando. Isso pode não ser um problema, mas é pelo menos algo para estar ciente.
O SQL dinâmico interrompe a cadeia de propriedade, o que significa que o acesso de leitura / gravação a tabelas não pode ser assumido com EXECUTE
permissão no Procedimento Armazenado. A correção fácil, mas menos segura, é apenas para fornecer ao usuário acesso direto às tabelas. Certamente isso não é o ideal, mas esse é geralmente o trade-off para rápido e fácil. A abordagem mais segura é usar a segurança baseada em certificado. Ou seja, criar um certificado, em seguida, criar um usuário de que Certificate, conceder que usuário as permissões desejadas (um usuário com base em certificado ou login não pode se conectar ao SQL Server por conta própria), e depois assinar os procedimentos armazenados que usam SQL dinâmico com que mesmo certificado via ADICIONAR ASSINATURA .
Para obter mais informações sobre assinatura e certificados de módulos, consulte: ModuleSigning.Info
(ID, TenantID)
e você também criar um índice não clusterizado(TenantID, ID)
ou simplesmente(TenantID)
possuir estatísticas precisas para consultas que processam a maioria das linhas de um único inquilino?