Vou postar uma resposta para começar. Meu primeiro pensamento foi que deveria ser possível tirar proveito da natureza de preservação de ordem de uma junção de loop aninhada, juntamente com algumas tabelas auxiliares que possuem uma linha para cada letra. A parte complicada seria fazer um loop de tal maneira que os resultados fossem ordenados por comprimento, além de evitar duplicatas. Por exemplo, ao ingressar em uma CTE que inclui todas as 26 letras maiúsculas junto com '', você pode acabar gerando 'A' + '' + 'A'
e, '' + 'A' + 'A'
é claro, a mesma string.
A primeira decisão foi onde armazenar os dados auxiliares. Tentei usar uma tabela temporária, mas isso teve um impacto surpreendentemente negativo no desempenho, mesmo que os dados se encaixem em uma única página. A tabela temporária continha os dados abaixo:
SELECT 'A'
UNION ALL SELECT 'B'
...
UNION ALL SELECT 'Y'
UNION ALL SELECT 'Z'
Comparada ao uso de um CTE, a consulta demorou 3 vezes mais com uma tabela em cluster e 4X mais com uma pilha. Não acredito que o problema seja que os dados estejam no disco. Ele deve ser lido na memória como uma única página e processado na memória para todo o plano. Talvez o SQL Server possa trabalhar com dados de um operador Constant Scan com mais eficiência do que com dados armazenados em páginas típicas de armazenamento de linhas.
Curiosamente, o SQL Server escolhe colocar os resultados ordenados de uma única tabela tempdb de página com dados ordenados em um spool de tabela:
O SQL Server geralmente coloca os resultados da tabela interna de uma junção cruzada em um spool de tabela, mesmo que pareça absurdo fazê-lo. Eu acho que o otimizador precisa de um pouco de trabalho nessa área. Eu executei a consulta com o NO_PERFORMANCE_SPOOL
para evitar o impacto no desempenho.
Um problema ao usar um CTE para armazenar os dados auxiliares é que não é garantido que os dados sejam solicitados. Não consigo pensar por que o otimizador escolheria não solicitá-lo e, em todos os meus testes, os dados foram processados na ordem em que escrevi o CTE:
No entanto, é melhor não arriscar, especialmente se houver uma maneira de fazê-lo sem uma grande sobrecarga de desempenho. É possível solicitar os dados em uma tabela derivada adicionando um TOP
operador supérfluo . Por exemplo:
(SELECT TOP (26) CHR FROM FIRST_CHAR ORDER BY CHR)
Essa adição à consulta deve garantir que os resultados sejam retornados na ordem correta. Eu esperava que todos os tipos tivessem um grande impacto negativo no desempenho. O otimizador de consultas esperava isso também com base nos custos estimados:
Surpreendentemente, não pude observar nenhuma diferença estatisticamente significante no tempo ou no tempo de execução da CPU, com ou sem ordenação explícita. Se alguma coisa, a consulta parecia correr mais rápido com o ORDER BY
! Não tenho explicação para esse comportamento.
A parte complicada do problema foi descobrir como inserir caracteres em branco nos lugares certos. Como mencionado anteriormente, um simples CROSS JOIN
resultaria em dados duplicados. Sabemos que a sequência 100000000 terá um comprimento de seis caracteres porque:
26 + 26 ^ 2 + 26 ^ 3 + 26 ^ 4 + 26 ^ 5 = 914654 <100000000
mas
26 + 26 ^ 2 + 26 ^ 3 + 26 ^ 4 + 26 ^ 5 + 26 ^ 6 = 321272406> 100000000
Portanto, só precisamos aderir à carta CTE seis vezes. Suponha que nos juntemos ao CTE seis vezes, pegue uma letra de cada CTE e concatene-as todas juntas. Suponha que a letra mais à esquerda não esteja em branco. Se alguma das letras subseqüentes estiver em branco, isso significa que a cadeia tem menos de seis caracteres e, portanto, é uma duplicata. Portanto, podemos evitar duplicatas localizando o primeiro caractere que não está em branco e exigindo todos os caracteres depois que ele também não estiver em branco. Eu escolhi acompanhar isso atribuindo uma FLAG
coluna a um dos CTEs e adicionando uma verificação à WHERE
cláusula. Isso deve ficar mais claro depois de analisar a consulta. A consulta final é a seguinte:
WITH FIRST_CHAR (CHR) AS
(
SELECT 'A'
UNION ALL SELECT 'B'
UNION ALL SELECT 'C'
UNION ALL SELECT 'D'
UNION ALL SELECT 'E'
UNION ALL SELECT 'F'
UNION ALL SELECT 'G'
UNION ALL SELECT 'H'
UNION ALL SELECT 'I'
UNION ALL SELECT 'J'
UNION ALL SELECT 'K'
UNION ALL SELECT 'L'
UNION ALL SELECT 'M'
UNION ALL SELECT 'N'
UNION ALL SELECT 'O'
UNION ALL SELECT 'P'
UNION ALL SELECT 'Q'
UNION ALL SELECT 'R'
UNION ALL SELECT 'S'
UNION ALL SELECT 'T'
UNION ALL SELECT 'U'
UNION ALL SELECT 'V'
UNION ALL SELECT 'W'
UNION ALL SELECT 'X'
UNION ALL SELECT 'Y'
UNION ALL SELECT 'Z'
)
, ALL_CHAR (CHR, FLAG) AS
(
SELECT '', 0 CHR
UNION ALL SELECT 'A', 1
UNION ALL SELECT 'B', 1
UNION ALL SELECT 'C', 1
UNION ALL SELECT 'D', 1
UNION ALL SELECT 'E', 1
UNION ALL SELECT 'F', 1
UNION ALL SELECT 'G', 1
UNION ALL SELECT 'H', 1
UNION ALL SELECT 'I', 1
UNION ALL SELECT 'J', 1
UNION ALL SELECT 'K', 1
UNION ALL SELECT 'L', 1
UNION ALL SELECT 'M', 1
UNION ALL SELECT 'N', 1
UNION ALL SELECT 'O', 1
UNION ALL SELECT 'P', 1
UNION ALL SELECT 'Q', 1
UNION ALL SELECT 'R', 1
UNION ALL SELECT 'S', 1
UNION ALL SELECT 'T', 1
UNION ALL SELECT 'U', 1
UNION ALL SELECT 'V', 1
UNION ALL SELECT 'W', 1
UNION ALL SELECT 'X', 1
UNION ALL SELECT 'Y', 1
UNION ALL SELECT 'Z', 1
)
SELECT TOP (100000000)
d6.CHR + d5.CHR + d4.CHR + d3.CHR + d2.CHR + d1.CHR
FROM (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d6
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d5
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d4
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d3
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d2
CROSS JOIN (SELECT TOP (26) CHR FROM FIRST_CHAR ORDER BY CHR) d1
WHERE (d2.FLAG + d3.FLAG + d4.FLAG + d5.FLAG + d6.FLAG) =
CASE
WHEN d6.FLAG = 1 THEN 5
WHEN d5.FLAG = 1 THEN 4
WHEN d4.FLAG = 1 THEN 3
WHEN d3.FLAG = 1 THEN 2
WHEN d2.FLAG = 1 THEN 1
ELSE 0 END
OPTION (MAXDOP 1, FORCE ORDER, LOOP JOIN, NO_PERFORMANCE_SPOOL);
Os CTEs são como descritos acima. ALL_CHAR
é associado a cinco vezes porque inclui uma linha para um caractere em branco. O caractere final da string nunca deve ficar em branco, para que um CTE separado seja definido para ele FIRST_CHAR
. A coluna de sinalizador extra ALL_CHAR
é usada para evitar duplicatas, conforme descrito acima. Pode haver uma maneira mais eficiente de fazer essa verificação, mas definitivamente existem maneiras mais ineficientes de fazer isso. Uma tentativa minha feita LEN()
e POWER()
executada a consulta seis vezes mais lenta que a versão atual.
As dicas MAXDOP 1
e FORCE ORDER
são essenciais para garantir que o pedido seja preservado na consulta. Um plano estimado anotado pode ser útil para ver por que as junções estão na ordem atual:
Os planos de consulta geralmente são lidos da direita para a esquerda, mas as solicitações de linha acontecem da esquerda para a direita. Idealmente, o SQL Server solicitará exatamente 100 milhões de linhas do d1
operador de verificação constante. Conforme você se move da esquerda para a direita, espero que menos linhas sejam solicitadas a cada operador. Podemos ver isso no plano de execução real . Além disso, abaixo está uma captura de tela do SQL Sentry Plan Explorer:
Temos exatamente 100 milhões de linhas de d1, o que é uma coisa boa. Observe que a proporção de linhas entre d2 e d3 é quase exatamente 27: 1 (165336 * 27 = 4464072), o que faz sentido se você pensar em como a união cruzada funcionará. A proporção de linhas entre d1 e d2 é 22,4, o que representa algum trabalho desperdiçado. Eu acredito que as linhas extras são de duplicatas (devido aos caracteres em branco no meio das cadeias) que não passam pelo operador de junção de loop aninhado que faz a filtragem.
A LOOP JOIN
dica é tecnicamente desnecessária porque a CROSS JOIN
só pode ser implementada como uma junção de loop no SQL Server. O NO_PERFORMANCE_SPOOL
objetivo é impedir o spool desnecessário da tabela. Omitir a dica do spool fez com que a consulta demorasse 3 vezes mais na minha máquina.
A consulta final tem um tempo de CPU de cerca de 17 segundos e um tempo total decorrido de 18 segundos. Isso foi ao executar a consulta através do SSMS e descartar o conjunto de resultados. Estou muito interessado em ver outros métodos de geração de dados.