Indici Datastore

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 gli indici manualmente 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, i join e le query aggregate non sono supportati all'interno del motore di query Datastore. Consulta la pagina Query Datastore per le limitazioni sulle 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 (ordine crescente o decrescente) per ogni proprietà. Per l'utilizzo con le query predecessori, l'indice può anche includere facoltativamente i predecessori di un'entità.

Una tabella di indice contiene una colonna per ogni proprietà denominata nella definizione dell'indice. Ogni riga della tabella rappresenta un'entità nel 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 fa riferimento a una proprietà per la quale l'entità non ha valore, l'entità non verrà visualizzata nell'indice e quindi non verrà mai restituita come risultato per qualsiasi query basata sull'indice.

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

  1. Proprietà utilizzate nei filtri di uguaglianza
  2. Proprietà utilizzata in un filtro di disuguaglianza (non più di una)
  3. Proprietà utilizzate negli ordinamenti

In questo modo, tutti i risultati di ogni possibile esecuzione della query vengono visualizzati in righe consecutive della tabella. Datastore esegue una query utilizzando un indice perfetto:

  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 la scansione dell'indice, restituendo ciascuna entità a turno, fino a quando
    • incontra un'entità che non soddisfa le condizioni di filtro oppure
    • raggiunge la fine dell'indice oppure
    • ha raccolto il numero massimo di risultati richiesti dalla query.

Ad esempio, considera la seguente query:

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

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

Per generare questi indici, configurali nel seguente modo:

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

Due query dello stesso formato ma con valori di filtro diversi utilizzano lo stesso indice. Ad esempio, la seguente query utilizza lo stesso indice di quello riportato sopra:

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

Anche le seguenti due query utilizzano lo stesso indice, nonostante le diverse forme:

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

Configurazione degli indici

Per impostazione predefinita, Datastore predefinisce automaticamente un indice per ogni proprietà di ogni tipo di entità. Questi indici predefiniti sono sufficienti per eseguire molte query semplici, come query di sola uguaglianza e query semplici sulla disuguaglianza. Per tutte le altre query, l'applicazione deve definire gli indici di cui ha bisogno in un file di configurazione indice 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 degli indici), la query avrà esito negativo.

Datastore crea indici automatici per le query nei formati seguenti:

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

Altre forme di query richiedono che i relativi indici siano specificati nel file di configurazione degli indici, tra cui:

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

Indici e proprietà

Di seguito sono riportate alcune considerazioni speciali da tenere presenti 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 valori diversi, un indice della proprietà ordina le entità prima in base al tipo di valore e poi in base a un ordinamento secondario appropriato a ogni tipo. Ad esempio, se due entità hanno ciascuna una proprietà denominata age, una con un valore intero e una con un valore stringa, l'entità con il valore intero precede sempre quella con il valore stringa quando viene ordinata in base alla proprietà age, indipendentemente dai valori della proprietà stessi.

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

Proprietà non indicizzate

Se sai che non dovrai mai filtrare o ordinare per una determinata proprietà, puoi indicare a Datastore di non mantenere le voci di indice relative a quella proprietà dichiarando che la proprietà è non indicizzata. Questo riduce 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 stata 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à e l'impostazione su Non indicizzato ne impedirà l'indicizzazione nell'indice composto.

Ad esempio, supponiamo che un'entità abbia le proprietà a e b e che tu voglia creare un indice in grado di soddisfare query come WHERE a ="bike" and b="red". Supponiamo inoltre 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, il datastore non creerà voci di indice per gli indici a e b, perciò la query WHERE a="bike" and b="red" non funzionerà. Affinché Datastore possa creare voci per gli indici a e b, sia a che b devono essere indicizzati.

Per dichiarare una proprietà non indicizzata, imposta noindex nel tag campo struct:

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

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 filtrate sulla proprietà non restituiranno queste entità esistenti, perché le entità non sono state scritte nell'indice della query al momento della creazione. Per rendere le entità accessibili dalle query future, devi riscriverle in Datastore in modo che vengano inserite negli indici appropriati. Ciò significa che devi eseguire le seguenti operazioni per ciascuna entità esistente:

  1. Recupera (ottieni) l'entità da Datastore.
  2. Scrivi (mettere) l'entità 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 quella proprietà continueranno a esistere fino a quando 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 limiti al numero e alla dimensione complessiva delle voci di indice che possono essere associate a una singola entità. Questi limiti sono grandi e la maggior parte delle applicazioni non è interessata. Tuttavia, in alcune circostanze potresti incontrare i limiti.

Come descritto sopra, Datastore crea una voce in un indice predefinito per ogni proprietà di ogni entità, ad eccezione dei campi []byte e di quelli che hai dichiarato esplicitamente come non indicizzato. La proprietà può anche essere inclusa in indici personalizzati aggiuntivi dichiarati nel file di configurazione di index.yaml. A condizione che un'entità non abbia proprietà elenco, avrà al massimo una voce in ogni indice personalizzato (per gli indici non predecessori) o una per ciascuno dei predecessori dell'entità (per gli indici predecessori). Ognuna di queste voci di indice deve essere aggiornata ogni volta che il valore della proprietà cambia.

Per una proprietà che ha un singolo valore per ogni entità, ogni possibile valore deve essere archiviato una sola volta per entità nell'indice predefinito della proprietà. Anche così, è possibile che un'entità con un numero elevato di proprietà a valore singolo superi la voce di indice o il limite di dimensione. Analogamente, un'entità che può avere più valori per la stessa proprietà richiede una voce di indice distinta per ogni valore; ancora una volta, 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à, ciascuna delle quali può assumere più valori. Per contenere una tale entità, l'indice deve includere una voce per ogni possibile combinazione di valori della proprietà. Gli indici personalizzati che fanno riferimento a più proprietà, ciascuna con più valori, possono "esplodere" in modo combinato, richiedendo un numero elevato di voci per un'entità con solo un numero relativamente ridotto di possibili valori di proprietà. Questi indici esplosivi possono aumentare notevolmente il costo di scrittura di un'entità nel datastore, a causa dell'elevato numero di voci di indice che devono essere aggiornate e può anche causare facilmente il superamento del limite di voce o dimensione dell'entità.

Considera la query

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

il 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à della proprietà X). Ad esempio, il seguente codice
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.
	}
}

crea un'entità con quattro valori per la proprietà x, tre valori per la proprietà y e date impostati sulla data corrente. Ciò richiederà 12 voci di indice, una per ogni possibile combinazione di valori di 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 indici esplosivi e suggerire un indice alternativo. Tuttavia, in tutte le altre circostanze (come la query definita in questo esempio), Datastore genererà un indice enorme. In questo caso, puoi aggirare l'indice enorme configurando manualmente un indice nel file di configurazione dell'indice:

indexes:
- kind: Widget
  properties:
  - name: X
  - name: Date
- kind: Widget
  properties:
  - name: Y
  - name: Date
In questo modo ridurrai il numero di voci a solo (|X| * |Date| + |Y| * |Date|) o a 7 voci invece di 12:

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

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

Qualsiasi operazione di put che causa il superamento della voce di indice o del limite di dimensione da parte di un indice non riuscirà e genererà un errore. Il testo dell'errore descrive quale limite è stato superato ("Too many indexed properties" o "Index entries too large") e quale indice personalizzato è la causa. Se crei un nuovo indice che supererebbe i limiti per qualsiasi entità al momento della creazione, le query sull'indice avranno esito negativo e l'indice verrà visualizzato con lo stato Error nella console Google Cloud. Per risolvere gli indici nello stato Error:

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

  2. Esegui questo comando dalla directory in cui si trova 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 questo comando dalla directory in cui si trova index.yaml per creare l'indice in Datastore:

    gcloud datastore indexes create index.yaml
    

Puoi evitare l'esplosione degli indici evitando le query che richiederebbero un indice personalizzato utilizzando una proprietà list. Come descritto sopra, sono incluse le query con più ordinamenti o query con una combinazione di filtri di uguaglianza e disuguaglianza.