Índices do Datastore

O App Engine predefine um índice simples em cada propriedade de uma entidade. Um aplicativo do App Engine pode definir outros índices personalizados em um arquivo de configuração de índice chamado index.yaml. O servidor de desenvolvimento adiciona sugestões a esse arquivo automaticamente quando encontra consultas não executáveis com os índices atuais. É possível ajustar os índices manualmente editando o arquivo antes de fazer upload do aplicativo.

Observação: o mecanismo de consulta baseado em índice é compatível com uma grande variedade de consultas e adequado à maioria dos aplicativos. No entanto, ele não aceita alguns tipos de consulta comuns em outras tecnologias de banco de dados. Especificamente, mesclagens e consultas agregadas não são compatíveis com o mecanismo de consulta do Datastore. Para conhecer as limitações das consultas do Datastore, consulte a página Consultas do Datastore.

Definição e estrutura dos índices

Em uma lista de propriedades de um determinado tipo de entidade, um índice é definido com uma ordem correspondente (ascendente ou descendente) para cada propriedade. Para uso com consultas de ancestral, o índice também inclui ancestrais de uma entidade.

Uma tabela de índice contém uma coluna para cada propriedade especificada na definição do índice. Cada linha da tabela representa uma entidade no Datastore que é um possível resultado das consultas baseadas em índice. Uma entidade só é incluída no índice se tiver um conjunto de valores indexado para cada propriedade usada no índice. Se a definição do índice se referir a uma propriedade em que a entidade não tem um valor, essa entidade não aparecerá no índice e, portanto, nunca será retornada como resultado de uma consulta baseada em índice.

Em uma tabela de índice, as linhas são classificadas primeiro pelo ancestral e depois pelos valores da propriedade, na ordem especificada na definição do índice. O índice perfeito, que permite a realização de uma consulta mais eficiente, é definido nas seguintes propriedades, na ordem:

  1. propriedades usadas nos filtros de igualdade;
  2. propriedade usada em um filtro de desigualdade, em que não é possível haver mais de um;
  3. propriedades usadas em ordens de classificação.

Isso garante que, para cada possível execução de consulta, todos os resultados apareçam na tabela em linhas consecutivas. O Datastore executa uma consulta usando um índice perfeito seguindo estas etapas:

  1. Identifica o índice correspondente ao tipo da consulta, às propriedades do filtro, aos operadores do filtro e às ordens de classificação.
  2. Verifica desde o início do índice até a primeira entidade que atenda a todas as condições do filtro da consulta.
  3. Continua verificando o índice, retornando uma entidade por vez até
    • encontrar uma entidade que não atenda às condições do filtro ou;
    • chegar ao fim do índice ou
    • coletar o número máximo de resultados solicitados pela consulta.

Por exemplo, pense na seguinte consulta:

SELECT * FROM Person WHERE LastName = "Smith"
                       AND Height < 72
                  ORDER BY Height DESC

O índice perfeito para esta consulta é uma tabela de chaves para entidades do tipo Person, com colunas para os valores das propriedades LastName e Height. O índice é classificado primeiro em ordem crescente por LastName e depois em ordem decrescente por Height.

Para gerar esses índices, configure os índices da seguinte maneira:

indexes:
- kind: Person
  properties:
  - name: LastName
    direction: asc
  - name: Height
    direction: desc

Duas consultas do mesmo formato, mas com valores de filtro diferentes, usam o mesmo índice. Por exemplo, a consulta a seguir usa o mesmo índice da consulta acima:

SELECT * FROM Person WHERE LastName = "Jones"
                       AND Height < 63
                     ORDER BY Height DESC

As duas consultas abaixo também usam o mesmo índice, mesmo tendo formatos diferentes:

SELECT * FROM Person WHERE LastName = "Friedkin"
                       AND FirstName = "Damian"
                     ORDER BY Height ASC

e

SELECT * FROM Person WHERE LastName = "Blair"
                  ORDER BY FirstName, Height ASC

Configuração de índice

Por padrão, para cada propriedade de cada tipo de entidade, um índice é automaticamente predefinido pelo Datastore. Esses índices predefinidos são suficientes para realizar muitas consultas simples, como as só de igualdade e as de desigualdade simples. Para as demais consultas, o aplicativo precisa definir os índices necessários em um arquivo de configuração de índice denominado index.yaml. Se o aplicativo tentar fazer uma consulta que não possa ser executada com os índices disponíveis, sejam eles predefinidos ou especificados no arquivo de configuração de índice, a consulta falhará.

O Datastore cria índices automáticos para consultas nos formatos de:

  • consultas sem tipo usando apenas filtros de chave e ancestral;
  • Consultas usando apenas filtros de igualdade e ancestral
  • Consultas usando apenas filtros de desigualdade, limitados a uma única propriedade
  • consultas usando apenas filtros de ancestral, filtros de igualdade em propriedades e filtros de desigualdade em chaves;
  • consultas sem filtros e apenas uma ordem de classificação em uma propriedade, crescente ou decrescente.

Outros formatos de consulta exigem a especificação dos índices no arquivo de configuração do índice, incluindo:

  • consultas com filtros de desigualdade e ancestral;
  • consultas com um ou mais filtros de desigualdade em uma propriedade e um ou mais filtros de igualdade em outras propriedades;
  • Consultas com uma ordem de classificação nas chaves, em ordem decrescente
  • consultas com várias ordens de classificação

Índices e propriedades

Veja, a seguir, algumas considerações especiais sobre os índices e a relação deles com as propriedades de entidades no Datastore:

Propriedades com tipos de valor mistos

Quando duas entidades têm propriedades com o mesmo nome, mas tipos de valor diferentes, um índice da propriedade classifica as entidades primeiro por tipo de valor e depois por uma ordenação secundária apropriada a cada tipo. Por exemplo, se duas entidades tiverem uma propriedade denominada age, uma com valor inteiro e outra com valor de string, a entidade com o valor inteiro sempre precederá aquela com o valor de string quando classificadas pela propriedade age, independentemente dos valores das propriedades.

Isso é especialmente válido no caso de números inteiros e de ponto flutuante, que são tratados como tipos separados pelo Datastore. Como todos os números inteiros são classificados antes dos flutuantes, uma propriedade com o valor inteiro 38 é classificada antes de outra com valor de ponto flutuante 37.5.

Propriedades não indexadas

Caso não seja necessário filtrar ou classificar uma determinada propriedade, declare-a no Datastore como não indexada para que ele não mantenha as entradas de índice dessa propriedade. Isso reduz o custo de execução do aplicativo diminuindo o número de gravações do Datastore que ele precisa executar. Uma entidade com propriedade não indexada se comporta como se a propriedade não estivesse definida: as consultas com um filtro ou ordem de classificação na propriedade não indexada nunca corresponderão a essa entidade.

Observação: configurar uma propriedade como não indexada, quando ela aparece em um índice composto por várias propriedades, evita que ela seja indexada no índice composto.

Por exemplo, suponha que uma entidade tenha as propriedades a e b e que você queira criar um índice capaz de satisfazer consultas como WHERE a ="bike" and b="red". Imagine também que você não se importa com as consultas WHERE a="bike" e WHERE b="red". Se você definir a como não indexada e criar um índice para a e b, o Datastore não criará entradas de índice para o índice a e b e, portanto, a consulta WHERE a="bike" and b="red" não funcionará. Para que o Datastore crie entradas para os índices a e b, ambos a e b precisam ser indexados.

Uma propriedade é declarada como não indexada se você configurar noindex na tag do campo struct (em inglês):

type Person struct {
	Name string
	Age  int `datastore:",noindex"`
}

No entanto, alterar uma propriedade excluída para indexada não afeta qualquer entidade criada antes dessa alteração. A filtragem de consultas na propriedade não retornará tais entidades porque, quando criadas, elas não foram gravadas no índice da consulta. Para facilitar o acesso às entidades nas próximas consultas, é preciso gravá-las novamente no Datastore para inserção nos índices apropriados. Ou seja, você precisa seguir estas etapas para cada uma dessas entidades:

  1. Recuperar (get) a entidade do Datastore.
  2. Gravar (put) a entidade de volta no Datastore.

Da mesma forma, alterar uma propriedade indexada para não indexada só afeta as entidades gravadas posteriormente no Datastore. As entradas de índice de qualquer entidade atual com essa propriedade continuarão a existir até que as entidades sejam atualizadas ou excluídas. Para evitar resultados indesejados, é preciso limpar o código de todas as consultas que filtram ou classificam pela propriedade que agora é "não indexada".

Limites dos índices

O Datastore impõe limites quanto ao número e tamanho geral das entradas de índice associadas a uma única entidade. Esses limites são amplos e a maioria dos aplicativos não é afetada. No entanto, há circunstâncias em que você encontrará esses limites.

Conforme descrito acima, o Datastore cria uma entrada em um índice predefinido para cada propriedade de cada entidade, exceto os campos []byte e aqueles declarados explicitamente como não indexados. A propriedade também pode ser incluída em outros índices personalizados declarados no arquivo de configuração index.yaml. Contanto que uma entidade não tenha propriedades de lista, ela terá no máximo uma entrada em cada índice personalizado (índices não ancestrais) ou uma para cada ancestral da entidade (índices ancestrais). Cada entrada de índice precisará ser atualizada cada vez que houver alteração no valor da propriedade.

Para uma propriedade com um único valor para cada entidade, cada valor possível precisa ser armazenado apenas uma vez por entidade no índice predefinido dessa propriedade. Mesmo assim, é possível que uma entidade com um grande número de propriedades de único valor exceda a entrada de índice ou limite de tamanho. Da mesma forma, uma entidade com diversos valores para a mesma propriedade requer uma entrada de índice individual para cada valor. Repetindo, caso tenha um grande número de valores possíveis, essa entidade excederá o limite de entradas.

A situação piora no caso de entidades com diversas propriedades em que cada uma delas aceita diversos valores. Para acomodar esse tipo de entidade, o índice precisa incluir uma entrada para cada combinação possível de valores de propriedades. Os índices personalizados que referenciam várias propriedades, cada uma com diversos valores, podem "explodir" de forma combinatória, exigindo grandes números de entradas para uma entidade com apenas um número relativamente pequeno de valores de propriedade possíveis. Esses índices em explosão podem aumentar drasticamente o custo da gravação de uma entidade no Datastore, devido ao grande número de entradas de índice que precisam ser atualizadas, além de poderem fazer com que a entidade exceda a entrada de índice ou o limite de tamanho.

Considere a consulta

SELECT * FROM Widget WHERE X=1 AND Y=2 ORDER BY Date

que faz com que o SDK sugira o seguinte índice:

indexes:
- kind: Widget
  properties:
  - name: X
  - name: Y
  - name: Date
Esse índice exigirá um total de |X| * |Y| * |Date| entradas para cada entidade (em que |X| denota o número de valores associados à entidade da propriedade X). Por exemplo, o código a seguir
type Widget struct {
	X    []int
	Y    []string
	Date time.Time
}

func f(ctx context.Context) {
	e2 := &Widget{
		X:    []int{1, 2, 3, 4},
		Y:    []string{"red", "green", "blue"},
		Date: time.Now(),
	}

	k := datastore.NewIncompleteKey(ctx, "Widget", nil)
	if _, err := datastore.Put(ctx, k, e2); err != nil {
		// Handle error.
	}
}

cria uma entidade com quatro valores para a propriedade x, três valores para a propriedade y e date definido com a data atual. Isso requer 12 entradas de índice, uma para cada combinação possível de valores de propriedade:

(1, "red", <now>) (1, "green", <now>) (1, "blue", <now>)

(2, "red", <now>) (2, "green", <now>) (2, "blue", <now>)

(3, "red", <now>) (3, "green", <now>) (3, "blue", <now>)

(4, "red", <now>) (4, "green", <now>) (4, "blue", <now>)

Quando a mesma propriedade é repetida várias vezes, o Datastore pode detectar índices em explosão e sugerir um índice alternativo. No entanto, em todas as outras circunstâncias (como a consulta definida neste exemplo), o Datastore gerará um índice em explosão. Nesse caso, é possível evitar isso. Basta configurar manualmente um índice no arquivo de configuração.

indexes:
- kind: Widget
  properties:
  - name: X
  - name: Date
- kind: Widget
  properties:
  - name: Y
  - name: Date
Isso reduz o número de entradas necessárias para apenas (|X| * |Date| + |Y| * |Date|) ou 7 entradas em vez de 12:

(1, <now>) (2, <now>) (3, <now>) (4, <now>)

("red", <now>) ("green", <now>) ("blue", <now>)

Qualquer operação "put" que faça com que um índice exceda a entrada de índice ou limite de tamanho apresentará falha com um erro. O texto do erro descreve o limite que foi excedido ("Too many indexed properties" ou "Index entries too large") e o índice personalizado que o causou. Se você criar um novo índice que exceda os limites de qualquer entidade quando criado, as consultas ao índice falharão e o índice aparecerá no estado Error no Console do Google Cloud. Para resolver índices no estado Error, siga estas instruções:

  1. Remova o índice no estado Error do arquivo index.yaml.

  2. Execute o comando a seguir no diretório em que o index.yaml está localizado para remover esse índice do Datastore:

    gcloud datastore indexes cleanup index.yaml
    
  3. Resolva a causa do erro. Por exemplo:

    • Reformule a definição do índice e as consultas correspondentes.
    • Remova as entidades que causam a explosão do índice.
  4. Adicione novamente o índice ao arquivo index.yaml.

  5. Execute o comando a seguir no diretório em que o index.yaml está localizado para criar o índice no Datastore:

    gcloud datastore indexes create index.yaml
    

Para evitar os índices em explosão, não faça consultas que exijam um índice personalizado usando uma propriedade de lista. Conforme descrito acima, isso inclui as consultas com diversas ordens de classificação ou com uma mistura de filtros de igualdade e desigualdade.