Depois de escrever esta resposta, eu provavelmente deveria sinalizar a pergunta como “muito ampla” - podemos conversar por séculos sobre várias estratégias; no final, uma referência terá que ser executada com seus dados.
Cada tag pode ser representada com eficiência por um número inteiro. Cada entidade possui um conjunto de tags. É importante escolher a implementação correta do conjunto - são possíveis árvores B e matrizes ordenadas. Com esse conjunto, faremos apenas testes de associação. Como ambas as estruturas fazem isso em O (log t) (com tags t por entidade), eu preferiria matrizes devido à sua representação mais densa.
Agora podemos filtrar todas as entidades em uma operação O (n · log t · p) , em que p é o comprimento médio do caminho na árvore de decisão predicada. Essa árvore de decisão pode ser ordenada para que uma decisão possa ser alcançada rapidamente. Sem dados estatísticos, só é possível fatorar uma subexpressão comum.
A ordem na qual as entidades são pesquisadas não é realmente importante. Por outro lado, pode ser vantajoso para classificá-lo de tal forma que as entidades a índices 0
para i
todos têm um certo tag, enquanto o resto não. Isso reduz o n ao procurar por essa tag específica (na árvore de decisão, esse deve ser o primeiro teste). Isso pode ser expandido para vários níveis, mas isso complica e leva O (2 k ) de memória com kníveis. Com vários níveis, as tags com maior ganho devem ser decididas primeiro, onde o ganho é o número de entidades que não precisam ser pesquisadas vezes a probabilidade de descartá-las. O ganho se torna máximo para chances 50:50 ou quando 50% das entidades possuem essa tag específica. Isso permitiria otimizar mesmo que os padrões de acesso não fossem conhecidos.
Você também pode criar conjuntos que indexam as entidades por cada tag usada - um conjunto com todas as entidades para T1
e o próximo para T2
. Uma otimização óbvia (espaço e tempo) é parar quando um conjunto contém mais da metade de todos os elementos e salvar os elementos que não possuem essa tag - dessa forma, a criação de índices para todas as tags ocupará menos ½ · n · t
espaço (com t tags no total). Observe que salvar conjuntos complementares pode dificultar outras otimizações. Mais uma vez, eu (ordenava) matrizes para os conjuntos.
Se você também representar suas entidades por meio de um intervalo inteiro, poderá compactar o espaço usado para os conjuntos de índices armazenando apenas o membro inicial e final de um intervalo contínuo. Em termos de implementação, isso provavelmente seria feito com um bit alto para indicar se uma entrada é uma entrada vinculada ao intervalo ou regular.
Se agora tivermos conjuntos de índices (e, portanto, estatísticas sobre as tags), podemos otimizar nossos predicados para que propriedades improváveis sejam testadas primeiro (estratégia à prova de falhas). Isso significa que, se T1
é comum e T2
raro, o predicado T1 & T2
deve ser avaliado através da iteração em todas as T2
entradas do conjunto de índices e testando cada elemento T1
.
Se usarmos matrizes classificadas para implementar os conjuntos de índices, muitas etapas de avaliação poderão ser implementadas como operações de mesclagem. T1 & T2
significa que pegamos as listas T1
e T2
, alocamos uma matriz de destino do tamanho das entradas maiores e executamos o seguinte algoritmo até que ambas estejam vazias: Se T1[0] < T2[0]
, então T1++
(descarte a cabeça). Se T1[0] > T2[0]
então T2++
. Se ambas as cabeças são iguais, então copiar esse número até a matriz de destino, e incrementar todos os três ponteiros ( T1
, T2
, destino). Se o predicado for T1 | T2
, nenhum elemento será descartado, mas o menor será copiado. Um predicado do formulário T1 & ¬T2
também pode ser implementado usando uma estratégia fusão, mas ¬T1
ou T1 | ¬T2
não pode.
Isso deve ser considerado ao solicitar a árvore de decisão predicada: Complementos devem ocorrer no RHS de um &
, ou no final, quando a contagem final está sendo determinada e os elementos reais não precisam ser examinados.
Sem usar conjuntos de índices, cada encadeamento pode filtrar sua parte das entidades e retornar a contagem de elementos que correspondem ao predicado, que podem ser resumidos. Ao usar conjuntos de índices, cada segmento receberá um nó na árvore de decisão. Ele usa dois fluxos de entrada que correspondem aos conjuntos ordenados e retorna um fluxo mesclado. Observe que cada nó na árvore de decisão tem um conjunto correspondente que representa todas as entidades que cumprem essa subexpressão e que, devido à ordem dos conjuntos, não é necessário conhecer o conjunto inteiro de uma só vez para mesclá-los .
Diferentes estratégias, como mesclar conjuntos indexados ou filtrar uma lista de entidades, podem ser combinadas até um certo grau. A filtragem tem um desempenho muito previsível. Se uma consulta for muito específica, para que o uso de conjuntos de índices reduza drasticamente o espaço de pesquisa, as operações de mesclagem poderão ser melhores. É importante observar que a fusão de muitos conjuntos grandes de entradas pode resultar em desempenho muito pior do que a pesquisa por força bruta. Um algoritmo muito otimizado escolherá uma estratégia adequada com base no tamanho da entrada, na estrutura da consulta e nos indicadores estatísticos.
Além disso, o armazenamento em cache de resultados parciais pode ser benéfico se for esperado que consultas semelhantes sejam executadas no futuro, mesmo que não acelerem a execução inicial.
T1
o mesmo objecto de referência paraE1
,E2
, etc?