Isso é um pouco de desvio do problema real. Se fornecer contexto ajudar, a geração desses dados pode ser útil para formas de teste de desempenho de processamento de strings, para gerar strings que precisam ter alguma operação aplicada a elas dentro de um cursor ou para gerar substituições de nomes anônimos exclusivos para dados confidenciais. Estou interessado apenas em maneiras eficientes de gerar os dados dentro de SQL Servers, por favor, não pergunte por que preciso gerar esses dados.
Vou tentar começar com uma definição um tanto formal. Uma string é incluída na série se consistir apenas em letras maiúsculas de A - Z. O primeiro termo da série é "A". A série consiste em todas as strings válidas classificadas primeiro por comprimento e depois por ordem alfabética típica. Se as strings estivessem em uma tabela em uma coluna chamada STRING_COL
, a ordem poderia ser definida em T-SQL como ORDER BY LEN(STRING_COL) ASC, STRING_COL ASC
.
Para dar uma definição menos formal, dê uma olhada nos cabeçalhos das colunas alfabéticas no Excel. A série é o mesmo padrão. Considere como você pode converter um número inteiro em um número de base 26:
1 -> A, 2 -> B, 3 -> C, ... , 25 -> Y, 26 -> Z, 27 -> AA, 28 -> AB, ...
A analogia não é perfeita porque "A" se comporta de maneira diferente de 0 na base dez. Abaixo está uma tabela de valores selecionados que esperamos tornar mais claro:
╔════════════╦════════╗
║ ROW_NUMBER ║ STRING ║
╠════════════╬════════╣
║ 1 ║ A ║
║ 2 ║ B ║
║ 25 ║ Y ║
║ 26 ║ Z ║
║ 27 ║ AA ║
║ 28 ║ AB ║
║ 51 ║ AY ║
║ 52 ║ AZ ║
║ 53 ║ BA ║
║ 54 ║ BB ║
║ 18278 ║ ZZZ ║
║ 18279 ║ AAAA ║
║ 475253 ║ ZZZY ║
║ 475254 ║ ZZZZ ║
║ 475255 ║ AAAAA ║
║ 100000000 ║ HJUNYV ║
╚════════════╩════════╝
O objetivo é escrever uma SELECT
consulta que retorne as primeiras 100000000 strings na ordem definida acima. Fiz meus testes executando consultas no SSMS com o conjunto de resultados descartado em vez de salvá-lo em uma tabela:
Idealmente, a consulta será razoavelmente eficiente. Aqui estou definindo eficiente como tempo de CPU para uma consulta serial e tempo decorrido para uma consulta paralela. Você pode usar quaisquer truques não documentados que desejar. Confiar em comportamento indefinido ou não garantido também é bom, mas seria apreciado se você chamasse isso em sua resposta.
Quais são alguns métodos para gerar eficientemente o conjunto de dados descrito acima? Martin Smith apontou que um procedimento armazenado CLR provavelmente não é uma boa abordagem devido à sobrecarga de processamento de tantas linhas.
Your solution runs for 35 seconds on my laptop. The following code takes 26 seconds (including creating and populating the temporary tables):
Temporary tables
The idea there is to pre-populate ordered combinations of up to four characters.
Main code
Essa é uma união simples de preservação de ordem* das quatro tabelas pré-calculadas, com strings de 5 e 6 caracteres derivadas conforme necessário. Separar o prefixo do sufixo evita a classificação.
Plano de execução
* Não há nada no SQL acima que especifique diretamente uma união que preserva a ordem . O otimizador escolhe operadores físicos com propriedades que correspondem à especificação de consulta SQL, incluindo ordem de nível superior por. Aqui, ele escolhe a concatenação implementada pelo operador físico merge join para evitar a classificação.
A garantia é que o plano de execução entrega a semântica da consulta e a ordem de nível superior por especificação. Saber que merge join concat preserva a ordem permite que o escritor de consultas antecipe um plano de execução, mas o otimizador só entregará se a expectativa for válida.
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 junto com algumas tabelas auxiliares que têm uma linha para cada letra. A parte complicada seria fazer um loop de tal forma que os resultados fossem ordenados por comprimento, além de evitar duplicatas. Por exemplo, ao fazer uma junção cruzada de um CTE que inclui todas as 26 letras maiúsculas junto com '', você pode acabar gerando
'A' + '' + 'A'
e'' + 'A' + 'A'
que obviamente é 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 caibam em uma única página. A tabela temporária continha os dados abaixo:
Comparado ao uso de um CTE, a consulta demorou 3 vezes mais com uma tabela em cluster e 4 vezes mais com um heap. Eu não acredito que o problema é que os dados estão 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 de rowstore típicas.
Curiosamente, o SQL Server opta por colocar os resultados ordenados de uma tabela tempdb de página única 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. Acho que o otimizador precisa de um pouco de trabalho nessa área. Executei a consulta com o
NO_PERFORMANCE_SPOOL
para evitar o impacto no desempenho.Um problema com o uso de um CTE para armazenar os dados auxiliares é que não há garantia de que os dados sejam ordenados. Não consigo pensar por que o otimizador optaria por não ordená-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 fazer isso sem uma grande sobrecarga de desempenho. É possível ordenar os dados em uma tabela derivada adicionando um
TOP
operador supérfluo. Por exemplo: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 também esperava isso com base nos custos estimados:
Surpreendentemente, não pude observar nenhuma diferença estatisticamente significativa no tempo de CPU ou tempo de execução 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 era descobrir como inserir caracteres em branco nos lugares certos. Como mencionado anteriormente, um simples
CROSS JOIN
resultaria em dados duplicados. Sabemos que a 100000000ª string terá um comprimento de seis caracteres porque:mas
Portanto, só precisamos juntar a letra CTE seis vezes. Suponha que nos juntemos ao CTE seis vezes, pegue uma letra de cada CTE e concatene-os todos juntos. Suponha que a letra mais à esquerda não esteja em branco. Se alguma das letras subsequentes estiver em branco, isso significa que a string tem menos de seis caracteres, portanto, é uma duplicata. Portanto, podemos evitar duplicatas encontrando o primeiro caractere não em branco e exigindo que todos os caracteres depois dele também não estejam em branco. Escolhi rastrear 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:Os CTEs são os descritos acima.
ALL_CHAR
é unido a cinco vezes porque inclui uma linha para um caractere em branco. O caractere final na string nunca deve ficar em branco, portanto, um CTE separado é definido para ele,FIRST_CHAR
. A coluna de sinalização extra emALL_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 fazê-lo. Uma tentativa minhaLEN()
ePOWER()
fez com que a consulta fosse seis vezes mais lenta que a versão atual.As dicas
MAXDOP 1
eFORCE ORDER
são essenciais para garantir que a ordem seja preservada 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 varredura constante. À medida que você se move da esquerda para a direita, espero que menos linhas sejam solicitadas de 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 junçã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 strings) que não passam do operador de junção de loop aninhado que faz a filtragem.
A
LOOP JOIN
dica é tecnicamente desnecessária porque aCROSS JOIN
só pode ser implementada como uma junção de loop no SQL Server. ONO_PERFORMANCE_SPOOL
objetivo é evitar o spool de tabela desnecessário. Omitir a dica de spool fez a consulta demorar 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 por meio do SSMS e descartar o conjunto de resultados. Estou muito interessado em ver outros métodos de geração de dados.
Eu tenho uma solução otimizada para obter o código da string para qualquer número específico até 217.180.147.158 (8 caracteres). Mas eu não posso bater o seu tempo:
Na minha máquina, com SQL Server 2014, sua consulta leva 18 segundos, enquanto a minha leva 3m 46s. Ambas as consultas usam o sinalizador de rastreamento não documentado 8690 porque 2014 não dá suporte à
NO_PERFORMANCE_SPOOL
dica.Aqui está o código:
O truque aqui é pré-computar onde as diferentes permutações começam:
The other trick used is to simply use sum to get to the right value instead of trying to concat. To achieve this I simply offset the digits from base 26 to base 256 and add the ascii value of 'A' for each digit. So we obtain the binary representation of the string we're looking for. After that some string manipulations complete the process.