Estou tentando classificar um resultado de uma tabela multilíngue. Eu gostaria que o algoritmo de classificação viesse de uma função, mas ele adiciona um impacto de 8% ao desempenho, pelo menos. Então, eu não tenho certeza de como fazer isso. Portanto, para a classificação estou usando este método descrito em um artigo sobre como classificar tabelas multilíngues conforme mostrado abaixo:
select UnicodeData,Collation
from (
select
ML.UnicodeData,
ML.Collation,
RN =
CASE
when Collation = 'he-IL' then ROW_NUMBER() OVER (order by unicodedata Collate Hebrew_CI_AS )
when Collation = 'en-US' then ROW_NUMBER() OVER (order by unicodedata Collate SQL_Latin1_General_CP1_CI_AS )
when Collation = 'kn-IN' then ROW_NUMBER() OVER (order by unicodedata Collate Indic_General_100_CI_AS )
when Collation = 'hi-IN' then ROW_NUMBER() OVER (order by unicodedata Collate Indic_General_100_CI_AS )
when Collation = 'ar-EG' then ROW_NUMBER() OVER (order by unicodedata Collate Arabic_CI_AS )
when Collation = 'cs' then ROW_NUMBER() OVER (order by unicodedata Collate Czech_CI_AS )
END
from MultipleLanguages ML
) T
order by RN
Exceto que eu abstraí o collation
código em sua própria função, assim:
CREATE FUNCTION [utils].[OrderByLanguage]
( @LanguageID tinyint
, @IDName utils.ID_Name READONLY
) RETURNS TABLE AS RETURN
SELECT
t.ID
, CASE @LanguageID
WHEN 1 THEN ROW_NUMBER() OVER (ORDER BY t.[Name]) -- en
WHEN 3 THEN ROW_NUMBER() OVER (ORDER BY t.[Name]) -- en-ca
WHEN 6 THEN ROW_NUMBER() OVER (ORDER BY t.[Name]) -- 'en-nz'
WHEN 5 THEN ROW_NUMBER() OVER (ORDER BY t.[Name]) -- 'en-za'
WHEN 2 THEN ROW_NUMBER() OVER (ORDER BY t.[Name] COLLATE Modern_Spanish_CI_AI) -- es
WHEN 4 THEN ROW_NUMBER() OVER (ORDER BY t.[Name] COLLATE French_CI_AI) -- 'fr-ca'
END RowNumber
FROM @IDName t
Mas então, quando eu chamo essa função, tenho que fazer essa chamada dupla estranha na função com valor de tabela.
CREATE FUNCTION api.GetTable
( @LanguageCode VARCHAR(10)
) RETURNS NVARCHAR(MAX)
AS BEGIN
DECLARE
@Result NVARCHAR(MAX)
, @LangID tinyint
DECLARE @Sort utils.ID_Name
SET @LangID = api_utils.GetLanguageID(@LanguageCode)
INSERT INTO @Sort (ID, [Name])
SELECT
t.ID
, t.title
FROM api_utils.GetTable(@LangID) t
SET @Result = (
SELECT
CONVERT(VARCHAR(10), t.ID) id,
t.category,
t.[system],
t.title,
JSON_QUERY(utils.ToRawJsonArray((
SELECT x.[Description]
FROM api_utils.GetKeywords(t.ID, @LangID) x
ORDER BY x.[Description]
FOR JSON AUTO), 'Description')
) keywords
FROM api_utils.GetTable(@LangID) t
ORDER BY (SELECT s.RowNumber
FROM utils.OrderByLanguage(@LangID, @Sort) s
WHERE s.ID = t.ID)
FOR JSON AUTO, ROOT('titles')
)
RETURN @Result
END
Então, você pode ver que eu tenho que chamar a função api_utils.GetTable
duas vezes. Até onde eu sei, a única outra maneira de abstrair a classificação de agrupamento seria colocar o algoritmo de classificação real e, em seguida, ter um script que pesquise toda a base de código e adicione outro idioma de agrupamento sempre que precisar adicionar outro idioma. Existe alguma outra maneira de fazer isso? O que os outros fizeram? Qual seria a melhor prática? O desempenho nisso não é absolutamente crítico, mas é bom mantê-lo enxuto para que não demore muito, pois já é uma chamada intensiva.
Desde já, obrigado!
Atualizar
Respondendo às perguntas de @srutzky nos comentários:
1) Quantos dados são retornados por api_utils.GetTable?
Há cerca de 150 registros retornados da tabela.
2) Por que chamar api_utils.GetTable duas vezes quando na primeira vez o resultado é despejado em @Sort?
A @Sort
tabela é uma tabela definida pelo usuário com otimização de memória ( UDT
). Como estou passando uma tabela para a utils.OrderByLanguage
função, ela precisa ser um arquivo UDT
. O que significa que preciso obter os dados da função inline api_utils.GetTable
duas vezes. Não tenho certeza se isso causou um problema de desempenho ao chamar api_utils.GetTable
duas vezes. Talvez SQL Server
seja inteligente o suficiente para armazenar em cache o resultado? Testando novamente o INSERT
custo da consulta é de 38%. Portanto, uma parte bastante significativa do custo da consulta.
Não seria mais rápido adicionar as colunas de categoria e sistema a @Sort e puxá-las de volta na primeira chamada e depois usar @Sort na cláusula FROM?
Como o UDT
é genérico para uso em todos os diferentes procedimentos que chamam a função utils.OrderByLanguage
, seria muito difícil generalizar para um número desconhecido de colunas que os diferentes procedimentos usariam.
3) Isso tem que ser uma função ou pode ser um procedimento armazenado?
Você está falando api_utils.GetTable
? Eu preferiria que api_utils.GetTable
permanecesse uma função, pois facilita o uso e o teste. Onde eu chamo api_utils.GetTable
é um Stored Procedure
.
Se você está falando utils.OrderByLanguage
, eu não me importo se seria um arquivo stored procedure
. Eu não tenho certeza de como isso ajudaria embora. Então, se for, por favor me avise!
Atualização para resposta aceita
Descobri que adicionar um índice não fazia diferença no desempenho. Eu também imaginei que poderia colocar a sort
coluna na #sort
tabela original, pois ela deve ser a mesma de qualquer maneira. Isso reduz o número de avisos no meu projeto SSDT. Então eu apenas faço um alter
na coluna assim:
ALTER TABLE #AlterSort ALTER COLUMN [sort] nvarchar(max) COLLATE SQL_Latin1_General_CP1_CI_AS
Olhando para o que você tem até agora, é bom que
[utils].[OrderByLanguage]
seja uma função com valor de tabela em linha (ITVF), mas ainda parece ser uma subconsulta correlacionada onde, para cada linhaapi_utils.GetTable(@LangID)
, passa em todas as linhasapi_utils.GetTable(@LangID)
para classificá-la (ou seja, aORDER BY
cláusula.Aplicar agrupamentos em tempo de execução pode ser bastante caro, pois é necessário gerar as chaves de classificação para esses valores naquele momento. Para obter o melhor desempenho, a criação de um índice gerará as chaves de classificação antecipadamente e até as colocará na ordem correta. Mas lidar com várias localidades é realmente complicado. Criar uma cópia da coluna de origem para cada localidade pode exigir uma enorme quantidade de espaço adicional em disco (e E/S), dependendo do tamanho das strings, de quantas localidades / agrupamentos são necessários e quantas linhas haverá os próximos 3 - 5 anos. Felizmente, você pode criar colunas computadas não persistentes para serem essas cópias (que não ocupam espaço) e indexá-las (o que ocupa espaço). Embora isso provavelmente não seja viável se houvesse 10 localidades, uma coluna de base de pelo menos
NVARCHAR(200)
, e 1 milhão (ou mais) de linhas, para sua situação deve funcionar bem. Com essa abordagem, a parte dinâmica estará na escolha de qual coluna selecionar (alcançável via SQL dinâmico ouIF
instruções, dependendo da situação). Mas, como você pode ver no exemplo a seguir (ative "Incluir Plano de Execução Real"), ambas as consultas filtradas (últimas 2 consultas) obtêm Buscas de Índice nos índices pretendidos e retornam os resultados esperados:Você pode ver uma demonstração ao vivo disso em dbfiddle.uk .
No entanto, dado o código para
api.GetTable
isso, pode não ser a melhor abordagem. Se você quiser manter a estrutura atual (tanto quanto possível), poderá fazer o seguinte:api.GetTable
em um procedimento armazenadoOUTPUT
parâmetro para não precisar lidar com um conjunto de resultados@Sort
deve ser uma tabela temporária, não uma variável de tabela#Sort
tabela temporária com todas as colunas necessárias, exceto /[Name]
)title
:ID
,category
e[system]
.Em uma série de
IF
declarações, adicione a[title]
coluna, mas com o Collation adequado:FROM api_utils.GetTable(@LangID) t
na@Result=
consulta principal para ser:FROM #Sort t
ORDER BY (SELECT s.RowNumber...
na@Result=
consulta principal para ser:ORDER BY t.[title]
Isso exigirá a reaplicação do Collation em cada execução, mas:
api.GetTable
(sem chamada dupla)[Name]
coluna[Name]
coluna depois que ela for preenchida.Reutilização de código
Ao mudar para procedimentos armazenados e tabelas temporárias, podemos realmente manter o objetivo de reutilização de código. O código para adicionar a coluna de classificação à tabela temporária local pode ser abstraído para outro procedimento armazenado. Embora a criação de uma tabela temporária em uma chamada de subprocedimento não ajude, pois essa tabela temporária desaparecerá quando a chamada de subprocedimento terminar, as alterações feitas em uma tabela temporária que existe antes da chamada de subprocedimento sobreviverão à conclusão dessa chamada . Por exemplo:
Configurar
Teste
Ao colocar os agrupamentos específicos apenas neste procedimento armazenado, você deve ter apenas um local para atualizar ao adicionar um novo idioma ao suporte.
Juntando tudo
Com todos os itens acima em mente, acabamos com o seguinte procedimento armazenado contendo uma única chamada para
api_utils.GetTable()
, sem subconsulta correlacionada eapi_utils.AddSortColumn
(que você precisa criar) pode ser usado em outros procedimentos armazenados e em outras tabelas base:Outra abordagem é ter apenas um gerador de código. Isso é se você precisar de desempenho máximo, pois é apenas cerca de 14% mais rápido que a solução marcada como a resposta para meus parâmetros fornecidos (dados 14% somados rapidamente quando você o tem em vários lugares e em várias partes do mesmo código) .