Datastore: [Parte 2] Queries and indexes

Queries 

O conceito de queries no datastore não é diferente do que já estamos acostumados com bancos relacionais. O importante é entender as limitações.
 
Mas, conceitualmente, queries retornam "entidades" do datastore que atendam aos critérios especificados na consulta.
 
Uma query normalmente inclui:
  
  • Uma "kind" (equivalente a uma tabela de banco relacional)
  • Um ou mais "filters": filtros baseados em valores das propriedades, chaves ou ancestrais.
  • Uma ou mais opções de ordenação ("sort")

Uma boa prática, com em qualquer banco de dados, é retornar o mínimo possível e necessário de informações, com o intuito de poupar recursos de memória e melhorar a performance.

A função dos Índices


Toda query do datastore utiliza um ou mais índices para ser processada. Atenção neste ponto. Esta é uma diferença fundamental, quando comparamos com bancos de dados relacionais, onde os índices tem a função primária de otimizar uma consulta (performance).

No datastore não. Os índices são parte do mecanismo de busca. Na verdade os índices são tabelas contendo entidades em um sequência especificada pelos índices ou ancestrais. Vamos falar mais sobre índices mais adiante.

Como utilizar queries com Python



A API Python do datastore permite utilizar queries de duas maneiras:

  • Query: utiliza métodos para preparar a query. Veja um exemplo da documentação do Google:
class Person(db.Model):
  first_name = db.StringProperty()
  last_name = db.StringProperty()
  city = db.StringProperty()
  birth_year = db.IntegerProperty()
  height = db.IntegerProperty()

# Query interface constructs a query using instance methods
q = Person.all()
q.filter("last_name =", "Smith")
q.filter("height <=", max_height)
q.order("-height")

# Query is not executed until results are accessed
for p in q.run(limit=5):
  print "%s %s, %d inches tall" % (p.first_name, p.last_name, p.height)

  • GqlQuery: utiliza uma sintaxe "SQL-Like". Veja o mesmo exemplo:
# GqlQuery interface constructs a query using a GQL query string
q = db.GqlQuery("SELECT * FROM Person " +
                "WHERE last_name = :1 AND height <= :2 " +
                "ORDER BY height DESC",
                "Smith", max_height)


Observação: todas as propriedades filtradas devem ter um índice correspondente criado através do index.yaml.
Filtros
Um filtro pode ser aplicado sobre:
  • properties
  • Keys
  • ancestors
Exemplo de filtro em cima de "Property":
 
q = Person.all()
q.filter("height <=", max_height)
 
Uma série de operadores podem ser utilizados (=, <, <=,>=, !=, IN).
 
Exemplo de filtro em cima de "Key":
q = Person.all()
q.filter('__key__ >', last_seen_key)
 
Exemplo de filtro em cima de "Ancestor":

q = Person.all()
q.ancestor(ancestor_key)
 

Ordenação

Veja alguns exemplos da documentação do Google:

# Order alphabetically by last name:
q = Person.all()
q.order('last_name')
# Order by height, tallest to shortest:
q = Person.all()
q.order('-height')


Exemplo de múltiplos "sorts" (um é aplicado após o outro):

q = Person.all()
q.order('lastName')
q.order('-height')

O uso do hyphen ("-") é utilizado para denotar que a ordenação será descendente.
 

Restrições de queries

 
  • Entidades sem uma determinada propriedade solicitada na query, será ignorada: como o datastore é um banco "schemaless" (sem esquema), nem todas as entidades terão a mesma estrutura. Se você fizer uma query solicitando uma determinada propriedade que não está em todas as entidades da "kind", somente serão retornadas as entidades que possuírem esta propriedade.
  • Filtrar por propriedades não "indexadas" não irá retornar nada. Os índices são fundamentais para os filtros.
  • Filtros do tipo "Inequality" (<, <=, >, >=, !=)  só funcionam para uma única propriedade:
SELECT * FROM Person WHERE birth_year >= :min_birth_year
                       AND birth_year <= :max_birth_year
Já esta query abaixo não irá funcionar:

SELECT * FROM Person WHERE birth_year >= :max_birth_year
                       AND height <= :max_height          # ERROR
Você pode combinar operadores de igualdade para propriedades diferentes, mas os de desigualdade tem que ser aplicados em cima da mesma propriedade, como abaixo:

SELECT * FROM Person WHERE last_name = :target_last_name
                       AND city = :target_city
                       AND birth_year >= :min_birth_year
                       AND birth_year <= :max_birth_year
  • Propriedades usadas em filtros de comparação  (<, <=, >, >=, !=)  devem ser ordenados primeiro:
Query válida:
SELECT * FROM Person WHERE birth_year >= :min_birth_year
                     ORDER BY birth_year, last_name
 
Query inválida:
SELECT * FROM Person WHERE birth_year >= :min_birth_year
                     ORDER BY last_name                   # ERROR
  • Queries dentro de transações devem incluir o "ancestor": transações no datastore operam somente em entidades de um mesmo "entity group". Para atender a esta restrição, você deve incluir o ancestor no filtro.

Retornando resultados de uma query

Retornando uma única entidade (retornará a primeira ocorrência):
q = Person.all()
q.filter("last_name =", target_last_name)
result = q.get()
 
Para retornar somente as chaves:
 
q = Person.all(keys_only=True)
Para limitar o retorno a 5 entidades (neste exemplo):
q = Person.all()
q.order("-height")
 
for p in q.run(limit=5):
  print "%s %s, %d inches tall" % (p.first_name, p.last_name, p.height)

Utilizando Cursor

Cursor, como em uma linguagem SQL tradicional, permite a uma query retornar um conjunto de registros do banco de dados, mantendo um ponteiro para o último resultado retornado. No mundo do datastore, este ponteiro é uma string base64, que pode ser armazenada no próprio datastore, no memcache ou "embedded" na própria página web. Esta string será passada no próximo request, para capturar o próximo conjundo de dados.

 Veja este exemplo em Python:
from google.appengine.api import memcache
from google.appengine.ext import db
 
# class Person(db.Model): ...
# Start a query for all Person entities
people = Person.all()
   
# If the application stored a cursor during a previous request, use it
person_cursor = memcache.get('person_cursor')
if person_cursor:
  people.with_cursor(start_cursor=person_cursor)
     
# Iterate over the results
for person in people:
  # Do something
     
# Get updated cursor and store it for next time
person_cursor = people.cursor()
memcache.set('person_cursor', person_cursor)

 
Consistência de Dados

 
No datastore as queries podem ser de 2 tipos:
  • Strongly consistent:consistência forte, ou seja, existe a garantia que os dados são "quentes".
  • Eventually consistent: queries rodam mais rápido, porém não existe garantia de consistência dos dados.
Queries que utilizam o "ancestor" (chave-pai) são "strongly consistent" por default. Sem o acestor as queries são sempre eventualmente consistentes.
A utilização de modelos strongly ou eventually dependem da natureza da aplicação que está sendo desenvolvida.

Comentários

Postagens mais visitadas deste blog

Entendendo o conceito de Error Budget: como o Google lida com o downtime

Analista de sistemas – z/VM

Abertura de vagas