Otimizar índices

Esta página descreve os conceitos a ter em conta ao selecionar os índices do Firestore no modo Datastore para a sua app.

O Firestore no modo Datastore oferece um elevado desempenho de consultas através da utilização de índices para todas as consultas. O desempenho da maioria das consultas depende da dimensão do conjunto de resultados e não da dimensão total da base de dados.

O Firestore no modo Datastore define índices incorporados para cada propriedade numa entidade. Estes índices de propriedade única suportam muitas consultas simples. O Firestore no modo Datastore suporta uma funcionalidade de união de índices que permite à sua base de dados unir índices incorporados para suportar consultas adicionais. Para consultas mais complexas, tem de definir índices compostos antecipadamente.

Esta página centra-se na funcionalidade de união de índices, porque afeta duas oportunidades importantes de otimização de índices:

  • Acelerar as consultas
  • Reduzir o número de índices compostos

O exemplo seguinte demonstra a funcionalidade de união de índices.

Filtrar entidades Photo

Considere uma base de dados do modo Datastore com entidades do tipo Photo:

Foto
Propriedade Tipo de valor Descrição
owner_id String ID do utilizador
tag Matriz de strings Palavras-chave tokenizadas
size Número inteiro Enumeração:
  • 1 icon
  • 2 medium
  • 3 large
coloration Número inteiro Enumeração:
  • 1 black & white
  • 2 color

Imagine que precisa de uma funcionalidade da app que permita aos utilizadores consultar Photo entidades com base num AND lógico do seguinte:

  • Até três filtros com base nas propriedades:

    • owner_id
    • size
    • coloration
  • Uma tag string de pesquisa. A app tokeniza a string de pesquisa em etiquetas e adiciona um filtro para cada etiqueta.

    Por exemplo, a app transforma a string de pesquisa outside, family na consulta filtros tag=outside e tag=family.

Ao usar os índices incorporados e a funcionalidade de união de índices do Firestore no modo Datastore, pode cumprir os requisitos de índice desta funcionalidade de filtro Photo sem adicionar índices compostos adicionais.

Os índices incorporados para entidades Photo suportam consultas com um único filtro, como:

Python

from google.cloud import datastore

# For help authenticating your client, visit
# https://cloud.google.com/docs/authentication/getting-started
client = datastore.Client()

query_owner_id = client.query(kind="Photo", filters=[("owner_id", "=", "user1234")])

query_size = client.query(kind="Photo", filters=[("size", "=", 2)])

query_coloration = client.query(kind="Photo", filters=[("coloration", "=", 2)])

A funcionalidade de filtro Photo também requer consultas que combinem vários filtros de igualdade com um AND lógico:

Python

from google.cloud import datastore

# For help authenticating your client, visit
# https://cloud.google.com/docs/authentication/getting-started
client = datastore.Client()

query_all_properties = client.query(
    kind="Photo",
    filters=[
        ("owner_id", "=", "user1234"),
        ("size", "=", 2),
        ("coloration", "=", 2),
        ("tag", "=", "family"),
    ],
)

O Firestore no modo Datastore pode suportar estas consultas através da união de índices incorporados.

União de índices

O Firestore no modo Datastore pode usar a união de índices quando a sua consulta e os seus índices cumprem todas as seguintes restrições:

  • A consulta usa apenas filtros de igualdade (=)
  • Não existe nenhum índice composto que corresponda perfeitamente aos filtros e à ordenação da consulta
  • Cada filtro de igualdade corresponde, pelo menos, a um índice existente com a mesma ordenação que a consulta

Nesta situação, o Firestore no modo Datastore pode usar os índices existentes para suportar a consulta em vez de exigir que configure um índice composto adicional.

Quando dois ou mais índices são ordenados pelos mesmos critérios, o Firestore no modo Datastore pode unir os resultados de várias análises de índices para encontrar os resultados comuns a todos esses índices. O Firestore no modo Datastore pode unir índices incorporados, porque todos ordenam os valores pela chave da entidade.

Ao unir índices incorporados, o Firestore no modo Datastore suporta consultas com filtros de igualdade em várias propriedades:

Python

from google.cloud import datastore

# For help authenticating your client, visit
# https://cloud.google.com/docs/authentication/getting-started
client = datastore.Client()

query_all_properties = client.query(
    kind="Photo",
    filters=[
        ("owner_id", "=", "user1234"),
        ("size", "=", 2),
        ("coloration", "=", 2),
        ("tag", "=", "family"),
    ],
)

O Firestore no modo Datastore também pode unir resultados de índice de várias secções do mesmo índice. Ao unir diferentes secções do índice incorporado para a propriedade tag, o Firestore no modo Datastore suporta consultas que combinam vários filtros tag numa lógica AND:

Python

from google.cloud import datastore

# For help authenticating your client, visit
# https://cloud.google.com/docs/authentication/getting-started
client = datastore.Client()

query_tag = client.query(
    kind="Photo",
    filters=[
        ("tag", "=", "family"),
        ("tag", "=", "outside"),
        ("tag", "=", "camping"),
    ],
)

query_owner_size_color_tags = client.query(
    kind="Photo",
    filters=[
        ("owner_id", "=", "user1234"),
        ("size", "=", 2),
        ("coloration", "=", 2),
        ("tag", "=", "family"),
        ("tag", "=", "outside"),
        ("tag", "=", "camping"),
    ],
)

As consultas suportadas por índices incorporados unidas completam o conjunto de consultas necessárias pela funcionalidade de filtragem Photo. Tenha em atenção que o suporte da funcionalidade de filtragem não exigiu índices compostos adicionais.Photo

Quando selecionar os índices ideais para a sua app, é importante compreender a funcionalidade de união de índices. A união de índices oferece ao Firestore no modo Datastore uma maior flexibilidade de consulta, mas com uma possível compensação no desempenho. A secção seguinte descreve o desempenho da união de índices e como melhorar o desempenho adicionando índices compostos.

Encontrar o índice perfeito

O índice é ordenado primeiro pelo antepassado e, em seguida, pelos valores das propriedades, na ordem especificada na definição do índice. O índice composto perfeito para uma consulta, que permite que a consulta seja executada da forma mais eficiente, é definido nas seguintes propriedades, por ordem:

  1. Propriedades usadas em filtros de igualdade
  2. Propriedades usadas em ordens de ordenação
  3. Propriedades usadas no filtro distinctOn
  4. Propriedades usadas em filtros de intervalo e desigualdade (que ainda não estão incluídas nas ordens de ordenação)
  5. Propriedades usadas em agregações e projeções (que ainda não estão incluídas em ordens de ordenação e filtros de intervalo e desigualdade)

Isto garante que todos os resultados para cada execução possível da consulta são contabilizados. As bases de dados do Firestore no modo Datastore executam uma consulta com um índice perfeito através dos seguintes passos:

  1. Identifica o índice correspondente ao tipo, às propriedades de filtro, aos operadores de filtro e às ordens de ordenação da consulta
  2. Analisa a partir do início do índice até à primeira entidade que cumpre todas ou um subconjunto das condições de filtro da consulta
  3. Continua a analisar o índice, devolvendo cada entidade que satisfaz todas as condições de filtro, até que
    • Encontra uma entidade que não cumpre as condições do filtro ou
    • Chega ao fim do índice ou
    • Recolheu o número máximo de resultados pedidos pela consulta

Por exemplo, considere a seguinte consulta:

SELECT * FROM Task
WHERE category = 'Personal'
  AND priority < 3
ORDER BY priority DESC

O índice composto perfeito para esta consulta é um índice de chaves para entidades do tipo Task, com colunas para os valores das propriedades category e priority. O índice é ordenado primeiro por ordem ascendente por category e, em seguida, por ordem descendente por priority:

indexes:
- kind: Task
  properties:
  - name: category
    direction: asc
  - name: priority
    direction: desc

Duas consultas do mesmo formulário, mas com valores de filtro diferentes, usam o mesmo índice. Por exemplo, a seguinte consulta usa o mesmo índice que a consulta anterior:

SELECT * FROM Task
WHERE category = 'Work'
  AND priority < 5
ORDER BY priority DESC

Para este índice

indexes:
- kind: Task
  properties:
  - name: category
    direction: asc
  - name: priority
    direction: asc
  - name: created
    direction: asc

O índice anterior pode satisfazer ambas as seguintes consultas:

SELECT * FROM Task
WHERE category = 'Personal'
  AND priority = 5
ORDER BY created ASC

e

SELECT * FROM Task
WHERE category = 'Work'
ORDER BY priority ASC, created ASC

Otimizar a seleção de índices

Esta secção descreve as caraterísticas de desempenho da união de índices e duas oportunidades de otimização relacionadas com a união de índices:

  • Adicione índices compostos para acelerar as consultas que dependem de índices unidos
  • Reduza o número de índices compostos tirando partido dos índices unidos

Desempenho da união de índices

Numa união de índices, o Firestore no modo Datastore une os índices de forma eficiente através de um algoritmo de junção de união em zigue-zague. Com este algoritmo, o modo Datastore junta potenciais correspondências de várias análises de índice para produzir um conjunto de resultados que corresponda a uma consulta. A união de índices combina componentes de filtro no momento da leitura, em vez de no momento da escrita. Ao contrário da maioria das consultas do Firestore no modo Datastore, em que o desempenho depende apenas do tamanho do conjunto de resultados, o desempenho das consultas de união de índices depende dos filtros na consulta e do número de potenciais correspondências que a base de dados considera.

O melhor desempenho possível de uma união de índices ocorre quando todas as potenciais correspondências num índice satisfazem os filtros de consulta. Neste caso, o desempenho é O(R * I), onde R é o tamanho do conjunto de resultados e I é o número de índices analisados.

O desempenho no pior cenário ocorre quando a base de dados tem de considerar muitas correspondências potenciais, mas poucas delas satisfazem os filtros de consulta. Neste caso, o desempenho é O(S), em que S é o tamanho do conjunto mais pequeno de potenciais entidades a partir de uma única análise de índice.

O desempenho real depende do formato dos dados. O número médio de entidades consideradas para cada resultado devolvido é O(S/(R * I)). As consultas têm um desempenho pior quando muitas entidades correspondem a cada análise de índice, mas poucas entidades correspondem à consulta como um todo, o que significa que R é pequeno e S é grande.

Quatro aspetos mitigam este risco:

  • O planeador de consultas não procura uma entidade até saber que a entidade corresponde à consulta completa.

  • O algoritmo em zigue-zague não precisa de encontrar todos os resultados para devolver o resultado seguinte. Se pedir os primeiros 10 resultados, paga apenas a latência para encontrar esses 10 resultados.

  • O algoritmo em ziguezague ignora grandes secções de resultados de falsos positivos. O pior desempenho só ocorre se os resultados falsos positivos estiverem perfeitamente interligados (por ordem de classificação) entre as análises.

  • A latência depende do número de entidades encontradas em cada análise de índice e não do número de entidades que correspondem a cada filtro. Conforme mostrado na secção seguinte, pode adicionar índices compostos para melhorar o desempenho da união de índices.

Acelerar uma consulta de união de índices

Quando o Firestore no modo Datastore une índices, cada análise de índice é frequentemente mapeada para um único filtro na consulta. Pode melhorar o desempenho das consultas adicionando índices compostos que correspondam a vários filtros na consulta.

Considere esta consulta:

Python

from google.cloud import datastore

# For help authenticating your client, visit
# https://cloud.google.com/docs/authentication/getting-started
client = datastore.Client()

query_owner_size_tag = client.query(
    kind="Photo",
    filters=[
        ("owner_id", "=", "username"),
        ("size", "=", 2),
        ("tag", "=", "family"),
    ],
)

Cada filtro é mapeado para uma análise de índice nos seguintes índices incorporados:

Index(Photo, owner_id)
Index(Photo, size)
Index(Photo, tag)

Se adicionar o índice composto Index(Photo, owner_id, size), a consulta é mapeada para duas análises de índice em vez de três:

#  Satisfies both 'owner_id=username' and 'size=2'
Index(Photo, owner_id, size)
Index(Photo, tag)

Considere um cenário com muitas imagens grandes, muitas imagens a preto e branco, mas poucas imagens panorâmicas grandes. Uma consulta que filtre imagens panorâmicas e a preto e branco é lenta se unir índices incorporados:

Python

from google.cloud import datastore

# For help authenticating your client, visit
# https://cloud.google.com/docs/authentication/getting-started
client = datastore.Client()

query_size_coloration = client.query(
    kind="Photo", filters=[("size", "=", 2), ("coloration", "=", 1)]
)

Para melhorar o desempenho das consultas, pode diminuir o valor de S (conjunto mais pequeno de entidades numa única análise de índice) em O(S/(R * I)) adicionando o seguinte índice composto:

Index(Photo, size, coloration)

Em comparação com a utilização de dois índices incorporados, este índice composto produz menos resultados potenciais para os mesmos dois filtros de consulta. Esta abordagem melhora substancialmente o desempenho ao custo de mais um índice.

Reduzir o número de índices compostos com a união de índices

Embora os índices compostos que correspondem exatamente aos filtros numa consulta tenham o melhor desempenho, nem sempre é melhor ou possível adicionar um índice composto para cada combinação de filtros. Tem de equilibrar os índices compostos com o seguinte:

  • Limites de índice composto:

    Limite Montante
    Número máximo de índices compostos para uma base de dados
    Soma máxima dos tamanhos das entradas do índice composto de uma entidade 2 MiB
    Soma máxima do seguinte para uma entidade:
    • O número de valores de propriedades indexados
    • o número de entradas de índice composto
    20 000
  • Custos de armazenamento de cada índice adicional.
  • Efeitos na latência de gravação.

Os problemas de indexação surgem frequentemente com campos de vários valores, como a propriedade tag das entidades Photo.

Por exemplo, imagine que a funcionalidade de filtragem Photo tem agora de suportar cláusulas de ordenação descendente com base em quatro propriedades adicionais:

Foto
Propriedade Tipo de valor Descrição
date_added Número inteiro Data/hora
rating Flutuante Classificação dos utilizadores agregada
comment_count Número inteiro Número de comentários
download_count Número inteiro Número de transferências

Se ignorar o campo tag, é possível selecionar índices compostos que correspondam a todas as combinações de filtros Photo:

Index(Photo, owner_id, -date_added)
Index(Photo, owner_id, -comments)
Index(Photo, size, -date_added)
Index(Photo, size, -comments)
...
Index(Photo, owner_id, size, -date_added)
Index(Photo, owner_id, size, -comments)
...
Index(Photo, owner_id, size, coloration, -date_added)
Index(Photo, owner_id, size, coloration, -comments)

O número total de índices compostos é:

2^(number of filters) * (number of different orders) = 2 ^ 3 * 4 = 32 composite indexes

Se tentar suportar até 3 filtros tag, o número total de índices compostos é:

2 ^ (3 + 3 tag filters) * 4 = 256 indexes.

Os índices que incluem propriedades com vários valores, como tag, também originam problemas de índice em expansão que aumentam os custos de armazenamento e a latência de escrita.

Para suportar filtros no campo tag para esta funcionalidade, pode reduzir o número total de índices com base em índices unidos. O seguinte conjunto de índices compostos é o mínimo necessário para suportar a funcionalidade de filtragem Photo com ordenação:

Index(Photo, owner_id, -date_added)
Index(Photo, owner_id, -rating)
Index(Photo, owner_id, -comments)
Index(Photo, owner_id, -downloads)
Index(Photo, size, -date_added)
Index(Photo, size, -rating)
Index(Photo, size, -comments)
Index(Photo, size, -downloads)
...
Index(Photo, tag, -date_added)
Index(Photo, tag, -rating)
Index(Photo, tag, -comments)
Index(Photo, tag, -downloads)

O número de índices compostos definidos é:

(number of filters + 1) * (number of orders) = 7 * 4 = 28

A união de índices também oferece as seguintes vantagens:

  • Permite que uma entidade Photo suporte até 1000 etiquetas sem limite no número de filtros tag por consulta.
  • Reduz o número total de índices, o que reduz os custos de armazenamento e a latência de escrita.

Selecionar índices para a sua app

Pode selecionar os índices ideais para a sua base de dados do modo Datastore através de duas abordagens:

  • Use a união de índices para suportar consultas adicionais

    • Requer menos índices compostos
    • Reduz o custo de armazenamento por entidade
    • Melhora a latência de escrita
    • Evita índices expansivos
    • O desempenho depende do formato dos dados
  • Defina um índice composto que corresponda a vários filtros numa consulta

    • Melhora o desempenho das consultas
    • Desempenho de consulta consistente que não depende do formato dos dados
    • Tem de permanecer abaixo do limite de índices compostos
    • Aumento do custo de armazenamento por entidade
    • Aumento da latência de escrita

Ao determinar os índices ideais para a sua app, a resposta pode mudar à medida que a forma dos seus dados muda. O desempenho das consultas de amostragem dá-lhe uma boa ideia das consultas comuns da sua app e das consultas lentas. Com estas informações, pode adicionar índices para melhorar o desempenho das consultas comuns e lentas.