Indici Datastore

Nota: gli sviluppatori che creano nuove applicazioni sono vivamente incoraggiati a utilizzare la libreria client NDB, che offre diversi vantaggi rispetto a questa libreria client, come la memorizzazione automatica nella cache delle entità tramite l'API Memcache. Se attualmente utilizzi la libreria client di DB precedente, leggi la guida alla migrazione da DB a NDB

App Engine predefinisce un indice semplice su ogni proprietà di un'entità. Un'applicazione App Engine può definire ulteriori indici personalizzati in un file di configurazione degli indici denominato index.yaml. Il server di sviluppo aggiunge automaticamente suggerimenti a questo file quando rileva query che non possono essere eseguite con gli indici esistenti. Puoi ottimizzare manualmente gli indici modificando il file prima di caricare l'applicazione.

Nota: il meccanismo di query basato su indice supporta un'ampia gamma di query ed è adatto alla maggior parte delle applicazioni. Tuttavia, non supporta alcuni tipi di query comuni in altre tecnologie di database: in particolare, join e query aggregate non sono supportati all'interno del motore di query di Datastore. Consulta la pagina Query Datastore per conoscere i limiti delle query Datastore.

Definizione e struttura dell'indice

Un indice è definito su un elenco di proprietà di un determinato tipo di entità, con un ordine corrispondente (crescente o decrescente) per ogni proprietà. Per l'utilizzo con le query sui predecessori, l'indice può anche includere i predecessori di un'entità.

Una tabella dell'indice contiene una colonna per ogni proprietà denominata nella definizione dell'indice. Ogni riga della tabella rappresenta un'entità in Datastore che è un potenziale risultato per le query basate sull'indice. Un'entità è inclusa nell'indice solo se ha un valore indicizzato impostato per ogni proprietà utilizzata nell'indice; se la definizione dell'indice si riferisce a una proprietà per la quale l'entità non ha valori, questa entità non verrà visualizzata nell'indice e, di conseguenza, non verrà mai restituita come risultato per qualsiasi query basata sull'indice.

Nota: Datastore distingue tra un'entità che non possiede una proprietà e una che possiede la proprietà con un valore nullo (None). Se assegni esplicitamente un valore nullo alla proprietà di un'entità, quest'ultima potrebbe essere inclusa nei risultati di una query che fa riferimento a tale proprietà.

Nota: gli indici composti da più proprietà richiedono che ogni singola proprietà non sia impostata su non indicizzato.

Le righe di una tabella di indice vengono ordinate prima per il predecessore e poi per i valori della proprietà, nell'ordine specificato nella definizione dell'indice. L'indice perfetto per una query, che ne consente l'esecuzione in modo più efficiente, viene definito nelle seguenti proprietà, in ordine:

  1. Proprietà utilizzate nei filtri di uguaglianza
  2. Proprietà utilizzata in un filtro di disuguaglianza (di cui non può esserci più di uno)
  3. Proprietà utilizzate negli ordinamenti

In questo modo, tutti i risultati per ogni possibile esecuzione della query verranno visualizzati in righe consecutive della tabella. Datastore esegue una query utilizzando un indice perfetto, seguendo questi passaggi:

  1. Identifica l'indice corrispondente al tipo, alle proprietà di filtro, agli operatori di filtro e agli ordini di ordinamento della query.
  2. Esegue la scansione dall'inizio dell'indice alla prima entità che soddisfa tutte le condizioni di filtro della query.
  3. Continua a scansionare l'indice, restituendo ogni entità alla volta, finché non
    • rileva un'entità che non soddisfa le condizioni di filtro
    • raggiunge la fine dell'indice o
    • ha raccolto il numero massimo di risultati richiesti dalla query.

Ad esempio, considera la seguente query (indicata in GQL):

SELECT * FROM Person WHERE last_name = "Smith"
                       AND height < 72
                  ORDER BY height DESC

L'indice perfetto per questa query è una tabella di chiavi per le entità di tipo Person, con colonne per i valori delle proprietà last_name e height. L'indice viene ordinato prima in ordine crescente per last_name e poi in ordine decrescente per height.

Per generare questi indici, configurali nel seguente modo:

indexes:
- kind: Person
  properties:
  - name: last_name
    direction: asc
  - name: height
    direction: desc

Due query dello stesso modulo, ma con valori di filtro diversi, utilizzano lo stesso indice. Ad esempio, la seguente query utilizza lo stesso indice di quella precedente:

SELECT * FROM Person WHERE last_name = "Jones"
                       AND height < 63
                     ORDER BY height DESC

Anche le due query seguenti utilizzano lo stesso indice, nonostante la loro forma diversa:

SELECT * FROM Person WHERE last_name = "Friedkin"
                       AND first_name = "Damian"
                     ORDER BY height ASC

and

SELECT * FROM Person WHERE last_name = "Blair"
                  ORDER BY first_name, height ASC

Configurazione degli indici

Per impostazione predefinita, Datastore predefinisce automaticamente un indice per ogni proprietà di ciascun tipo di entità. Questi indici predefiniti sono sufficienti per eseguire molte query semplici, come le query di sola uguaglianza e le semplici query di disuguaglianza. Per tutte le altre query, l'applicazione deve definire gli indici di cui ha bisogno in un file di configurazione degli indici denominato index.yaml. Se l'applicazione tenta di eseguire una query che non può essere eseguita con gli indici disponibili (predefiniti o specificati nel file di configurazione dell'indice), la query avrà esito negativo con un'eccezione NeedIndexError.

Datastore crea indici automatici per le query nei seguenti formati:

  • Query Kindless con filtri solo dei predecessori e delle chiavi
  • Query che utilizzano solo filtri dei predecessori e di uguaglianza
  • Query che utilizzano solo filtri di disuguaglianza (che sono limitati a una singola proprietà)
  • Query che utilizzano solo filtri predecessore, filtri di uguaglianza sulle proprietà e filtri di disuguaglianza nelle chiavi
  • Query senza filtri e con un solo ordinamento per proprietà, crescente o decrescente

Altre forme di query richiedono che i rispettivi indici siano specificati nel file di configurazione dell'indice, tra cui:

  • Query con filtri dei predecessori e della disuguaglianza
  • Query con uno o più filtri di disuguaglianza su una proprietà e uno o più filtri di uguaglianza su altre proprietà
  • Query con un ordinamento sulle chiavi in ordine decrescente
  • Query con più ordinamenti

Indici e proprietà

Di seguito sono riportate alcune considerazioni speciali da tenere a mente sugli indici e sulla loro relazione con le proprietà delle entità in Datastore:

Proprietà con tipi di valori misti

Quando due entità hanno proprietà con lo stesso nome ma tipi di valore diversi, un indice della proprietà ordina le entità prima per tipo di valore e poi in base a un ordine secondario appropriato per ciascun tipo. Ad esempio, se due entità hanno ciascuna una proprietà denominata age, una con un valore intero e una con un valore di stringa, l'entità con il valore intero precede sempre quella con il valore stringa quando viene ordinata dalla proprietà age, indipendentemente dai valori delle proprietà stesse.

Ciò è particolarmente degno di nota nel caso di numeri interi e in virgola mobile, che vengono trattati come tipi separati da Datastore. Poiché tutti i numeri interi vengono ordinati prima di tutti i numeri in virgola mobile, una proprietà con valore intero 38 viene ordinata prima di una proprietà con valore in virgola mobile 37.5.

Proprietà non indicizzate

Se sai che non dovrai mai filtrare o ordinare in base a una determinata proprietà, puoi indicare a Datastore di non gestire le voci di indice per quella proprietà dichiarando la proprietà non indicizzata. In questo modo si riducono i costi di esecuzione dell'applicazione, diminuendo il numero di scritture Datastore che deve eseguire. Un'entità con una proprietà non indicizzata si comporta come se la proprietà non fosse impostata: le query con un filtro o un ordinamento sulla proprietà non indicizzata non corrisponderanno mai a questa entità.

Nota: se una proprietà viene visualizzata in un indice composto da più proprietà, se la imposti come non indicizzata verrà impedita l'indicizzazione nell'indice composto.

Ad esempio, supponiamo che un'entità abbia le proprietà a e b e che voglia creare un indice in grado di soddisfare query come WHERE a ="bike" and b="red". Inoltre, supponiamo che le query WHERE a="bike" e WHERE b="red" non ti interessino. Se imposti a su non indicizzato e crei un indice per a e b, Datastore non creerà voci di indice per gli indici a e b e, di conseguenza, la query WHERE a="bike" and b="red" non funziona. Affinché Datastore crei voci per gli indici a e b, è necessario indicizzare sia a che b.

Dichiari una proprietà non indicizzata impostando indexed=False nel costruttore della proprietà:

class Person(db.Model):
  name = db.StringProperty()
  age = db.IntegerProperty(indexed=False)

In seguito puoi reimpostare la proprietà in indicizzata richiamando nuovamente il costruttore con indexed=True:

class Person(db.Model):
  name = db.StringProperty()
  age = db.IntegerProperty(indexed=True)

Tuttavia, tieni presente che la modifica di una proprietà da non indicizzata a indicizzata non influisce sulle entità esistenti che potrebbero essere state create prima della modifica. Le query che filtrano in base alla proprietà non restituiscono queste entità esistenti, perché non erano state scritte nell'indice della query al momento della creazione. Per rendere le entità accessibili per query future, devi riscriverle in Datastore in modo che vengano inserite negli indici appropriati. Ciò significa che devi procedere come segue per ciascuna di queste entità esistenti:

  1. Recupera (get) l'entità da Datastore.
  2. Scrivere (put) l'entità di nuovo in Datastore.

Analogamente, la modifica di una proprietà da indicizzata a non indicizzata interessa solo le entità scritte successivamente in Datastore. Le voci di indice per tutte le entità esistenti con la proprietà continueranno a esistere finché le entità non verranno aggiornate o eliminate. Per evitare risultati indesiderati, devi eliminare definitivamente il codice di tutte le query che filtrano o ordinano in base alla proprietà (ora non indicizzata).

Limiti indice

Datastore impone dei limiti al numero e alla dimensione complessiva delle voci di indice che possono essere associate a una singola entità. Questi limiti sono elevati e la maggior parte delle applicazioni non ne è interessato. Tuttavia, in alcune circostanze potresti riscontrare i limiti.

Come descritto sopra, Datastore crea una voce in un indice predefinito per ogni proprietà di ogni entità, ad eccezione delle stringhe di testo lunghe (Text) e delle stringhe di byte lunghi (Blob) e di quelle che hai dichiarato esplicitamente come non indicizzate. La proprietà può anche essere inclusa in altri indici personalizzati dichiarati nel file di configurazione index.yaml. Se un'entità non ha proprietà elenco, avrà al massimo una voce in ciascun indice personalizzato (per gli indici non predecessori) o una per ciascuno dei predecessori dell'entità (per gli indici dei predecessori). Ognuna di queste voci di indice va aggiornata ogni volta che cambia il valore della proprietà.

Per una proprietà che ha un singolo valore per ciascuna entità, ogni valore possibile deve essere archiviato una sola volta per entità nell'indice predefinito della proprietà. Anche in questo caso, è possibile che un'entità con un numero elevato di proprietà a un solo valore superi il limite di dimensioni o la voce di indice. Analogamente, un'entità che può avere più valori per la stessa proprietà richiede una voce di indice separata per ogni valore. Di nuovo, se il numero di valori possibili è elevato, un'entità di questo tipo può superare il limite di voci.

La situazione peggiora nel caso di entità con più proprietà, ognuna delle quali può assumere più valori. Per includere questa entità, l'indice deve includere una voce per ogni possibile combinazione di valori delle proprietà. Gli indici personalizzati che fanno riferimento a più proprietà, ciascuna con più valori, possono "esplodere" in modo combinatorio, richiedendo un numero elevato di voci per un'entità con solo un numero relativamente ridotto di possibili valori di proprietà. Il esplosione degli indici può aumentare notevolmente il costo di scrittura di un'entità in Datastore, a causa dell'elevato numero di voci di indice che devono essere aggiornate, nonché può causare facilmente il superamento della voce di indice o del limite di dimensione da parte dell'entità.

Considera la query

SELECT * FROM Widget WHERE x=1 AND y=2 ORDER BY date

che fa sì che l'SDK suggerisca il seguente indice:

indexes:
- kind: Widget
  properties:
  - name: x
  - name: y
  - name: date
Questo indice richiederà un totale di |x| * |y| * |date| voci per ogni entità (dove |x| indica il numero di valori associati all'entità per la proprietà x). Ad esempio, il seguente codice
class Widget(db.Expando):
  pass

e2 = Widget()
e2.x = [1, 2, 3, 4]
e2.y = ['red', 'green', 'blue']
e2.date = datetime.datetime.now()
e2.put()

crea un'entità con quattro valori per la proprietà x, tre valori per la proprietà y e date impostati sulla data corrente. Sono necessarie 12 voci di indice, una per ogni possibile combinazione di valori delle proprietà:

(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 la stessa proprietà viene ripetuta più volte, Datastore può rilevare gli indici in esplosione e suggerire un indice alternativo. Tuttavia, in tutte le altre circostanze (ad esempio, nella query definita in questo esempio), Datastore genererà un indice enorme. In questo caso, puoi eludere l'indice enorme configurandolo manualmente nel file di configurazione dell'indice:

indexes:
- kind: Widget
  properties:
  - name: x
  - name: date
- kind: Widget
  properties:
  - name: y
  - name: date
In questo modo viene ridotto il numero di voci necessarie a solo (|x| * |date| + |y| * |date|) oppure a 7 voci anziché a 12:

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

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

Qualsiasi operazione put che comporterebbe il superamento della voce o del limite di dimensioni di un indice non andrà a buon fine con un'eccezione BadRequestError. Il testo dell'eccezione descrive il limite superato ("Too many indexed properties" o "Index entries too large") e l'indice personalizzato che ne è la causa. Se crei un nuovo indice che al momento della creazione supererebbe i limiti per qualsiasi entità, le query sull'indice non riusciranno e l'indice verrà visualizzato nello stato Error nella console Google Cloud. Per risolvere gli indici nello stato Error:

  1. Rimuovi l'indice in stato Error dal file index.yaml.

  2. Esegui il comando seguente dalla directory in cui si trova il file index.yaml per rimuovere l'indice da Datastore:

    gcloud datastore indexes cleanup index.yaml
    
  3. Risolvi la causa dell'errore. Ad esempio:

    • Riformula la definizione dell'indice e le query corrispondenti.
    • Rimuovi le entità che causano l'esplosione dell'indice.
  4. Aggiungi di nuovo l'indice al file index.yaml.

  5. Esegui il comando seguente dalla directory in cui si trova l'oggetto index.yaml per creare l'indice in Datastore:

    gcloud datastore indexes create index.yaml
    

Per evitare l'esplosione degli indici, evita le query che richiederebbero un indice personalizzato utilizzando una proprietà elenco. Come descritto in precedenza, sono incluse le query con più ordinamenti o query con una combinazione di filtri di uguaglianza e disuguaglianza.