Í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 datastore-indexes.xml, que é gerado no diretório /war/WEB-INF/appengine-generated do seu aplicativo. 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.

Veja uma discussão mais detalhada sobre índices e consultas em Seleção de índice e pesquisa avançada.

Observação: o mecanismo de consulta baseado em índice é compatível com uma grande variedade de consultas, sendo 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 do índice

Em uma lista de propriedades de um determinado tipo de entidade, um índice é definido com uma ordem correspondente (crescente ou decrescente) 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.

Observação: o Datastore distingue uma entidade que não tem uma propriedade de uma que tem a propriedade com valor nulo (null). Se você atribuir explicitamente um valor nulo à propriedade de uma entidade, a entidade poderá ser incluída nos resultados de uma consulta referente a essa propriedade.

Observação: em índices compostos por várias propriedades, nenhuma delas pode ser definida como não indexada.

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 em 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, considere a seguinte consulta:

Query q1 =
    new Query("Person")
        .setFilter(
            CompositeFilterOperator.and(
                new FilterPredicate("lastName", FilterOperator.EQUAL, "Smith"),
                new FilterPredicate("height", FilterOperator.EQUAL, 72)))
        .addSort("height", Query.SortDirection.DESCENDING);

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.

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:

Query q2 =
    new Query("Person")
        .setFilter(
            CompositeFilterOperator.and(
                new FilterPredicate("lastName", FilterOperator.EQUAL, "Jones"),
                new FilterPredicate("height", FilterOperator.EQUAL, 63)))
        .addSort("height", Query.SortDirection.DESCENDING);

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

Query q3 =
    new Query("Person")
        .setFilter(
            CompositeFilterOperator.and(
                new FilterPredicate("lastName", FilterOperator.EQUAL, "Friedkin"),
                new FilterPredicate("firstName", FilterOperator.EQUAL, "Damian")))
        .addSort("height", Query.SortDirection.ASCENDING);

e

Query q4 =
    new Query("Person")
        .setFilter(new FilterPredicate("lastName", FilterOperator.EQUAL, "Blair"))
        .addSort("firstName", Query.SortDirection.ASCENDING)
        .addSort("height", Query.SortDirection.ASCENDING);

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 datastore-indexes.xml. Se o aplicativo tentar executar 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á com DatastoreNeedIndexException.

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

  • 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.

Na API Java Datastore de baixo nível, as propriedades são definidas como indexadas ou não indexadas de acordo com a entidade, dependendo do método usado para defini-las (setProperty() ou setUnindexedProperty()):

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

Key acmeKey = KeyFactory.createKey("Company", "Acme");

Entity tom = new Entity("Person", "Tom", acmeKey);
tom.setProperty("name", "Tom");
tom.setProperty("age", 32);
datastore.put(tom);

Entity lucy = new Entity("Person", "Lucy", acmeKey);
lucy.setProperty("name", "Lucy");
lucy.setUnindexedProperty("age", 29);
datastore.put(lucy);

Filter ageFilter = new FilterPredicate("age", FilterOperator.GREATER_THAN, 25);

Query q = new Query("Person").setAncestor(acmeKey).setFilter(ageFilter);

// Returns tom but not lucy, because her age is unindexed
List<Entity> results = datastore.prepare(q).asList(FetchOptions.Builder.withDefaults());

Para alterar uma propriedade indexada para não indexada, redefina o valor dela com setUnindexedProperty(). Para alterar de não indexada para indexada, redefina o valor com setProperty().

No entanto, observe que alterar uma propriedade não indexada para indexada não afeta entidades existentes criadas 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 à quantidade e ao 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 strings de texto longas (Text), strings de bytes longas (Blob) e entidades incorporadas (EmbeddedEntity), bem como as que você declarou explicitamente como não indexadas. A propriedade também pode ser incluída em outros índices personalizados declarados no arquivo de configuração datastore-indexes.xml. 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

Query q =
    new Query("Widget")
        .setFilter(
            CompositeFilterOperator.and(
                new FilterPredicate("x", FilterOperator.EQUAL, 1),
                new FilterPredicate("y", FilterOperator.EQUAL, 2)))
        .addSort("date", Query.SortDirection.ASCENDING);

que faz com que o SDK sugira o índice a seguir:

<?xml version="1.0" encoding="utf-8"?>
<datastore-indexes autoGenerate="false">
  <datastore-index kind="Widget" ancestor="false" source="manual">
    <property name="x" direction="asc"/>
    <property name="y" direction="asc"/>
    <property name="date" direction="asc"/>
  </datastore-index>
</datastore-indexes>
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
Entity widget = new Entity("Widget");
widget.setProperty("x", Arrays.asList(1, 2, 3, 4));
widget.setProperty("y", Arrays.asList("red", "green", "blue"));
widget.setProperty("date", new Date());
datastore.put(widget);

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 o índice em explosão configurando manualmente um índice em seu arquivo de configuração de índice:

<?xml version="1.0" encoding="utf-8"?>
<datastore-indexes autoGenerate="false">
  <datastore-index kind="Widget">
    <property name="x" direction="asc" />
    <property name="date" direction="asc" />
  </datastore-index>
  <datastore-index kind="Widget">
    <property name="y" direction="asc" />
    <property name="date" direction="asc" />
  </datastore-index>
</datastore-indexes>
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 o limite de entradas ou de tamanho falhará com IllegalArgumentException . O texto da exceção descreve o limite que foi excedido ("Too many indexed properties" ou "Index entries too large") e o índice personalizado que a 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 Cloud. Para resolver índices no estado Error:

  1. Remova o índice no estado Error do arquivo datastore-indexes.xml.

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

    gcloud datastore cleanup-indexes datastore-indexes.xml
    
  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 datastore-indexes.xml.

  5. Execute o comando a seguir no diretório em que o datastore-indexes.xml está localizado para criar o índice no Datastore:

    gcloud datastore create-indexes datastore-indexes.xml
    

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.