Ottimizzazione degli indici

Questa pagina descrive i concetti da considerare quando selezioni gli indici di Firestore in modalità Datastore per la tua app.

Firestore in modalità Datastore offre prestazioni elevate delle query utilizzando gli indici per tutte le query. Le prestazioni della maggior parte delle query dipendono dalle dimensioni del set di risultati e non dalle dimensioni totali del database.

Firestore in modalità Datastore definisce gli indici incorporati per ogni proprietà di un'entità. Questi indici di proprietà singola supportano molte query semplici. Firestore in modalità Datastore supporta una funzionalità di unione degli indici che consente al database di unire gli indici integrati per supportare query aggiuntive. Per query più complesse, devi definire in anticipo gli indici composti.

Questa pagina è incentrata sulla funzionalità di unione degli indici, in quanto influisce su due importanti opportunità di ottimizzazione degli indici:

  • Velocizzazione delle query
  • Riduzione del numero di indici composti

L'esempio seguente mostra la funzionalità di unione degli indici.

Filtrare le entità Photo

Prendi in considerazione un database in modalità Datastore con entità di tipo Photo:

Foto
Proprietà Tipo di valore Descrizione
owner_id Stringa ID utente
tag Array di stringhe Parole chiave tokenizzate
size Numero intero Enumerazione:
  • 1 icon
  • 2 medium
  • 3 large
coloration Numero intero Enumerazione:
  • 1 black & white
  • 2 color

Immagina di avere bisogno di una funzionalità dell'app che consenta agli utenti di eseguire query sulle Photo entità in base a un AND logico tra quanto segue:

  • Fino a tre filtri in base alle proprietà:

    • owner_id
    • size
    • coloration
  • Una stringa di ricerca tag. L'app tokenizza la stringa di ricerca in tag e aggiunge un filtro per ogni tag.

    Ad esempio, l'app trasforma la stringa di ricerca outside, family nei filtri di query tag=outside e tag=family.

Utilizzando gli indici integrati e la funzionalità di unione degli indici di Firestore in modalità Datastore, puoi soddisfare i requisiti degli indici di questa funzionalità di filtro Photo senza aggiungere altri indici composti.

Gli indici incorporati per le entità Photo supportano query con un solo filtro come:

Python

from google.cloud import datastore

# For help authenticating your client, visit
# https://cloud.google.com/docs/authentication/getting-started
client = datastore.Client()

query_owner_id = client.query(kind="Photo", filters=[("owner_id", "=", "user1234")])

query_size = client.query(kind="Photo", filters=[("size", "=", 2)])

query_coloration = client.query(kind="Photo", filters=[("coloration", "=", 2)])

La funzionalità di filtro Photo richiede anche query che combinano più filtri di uguaglianza con un AND logico:

Python

from google.cloud import datastore

# For help authenticating your client, visit
# https://cloud.google.com/docs/authentication/getting-started
client = datastore.Client()

query_all_properties = client.query(
    kind="Photo",
    filters=[
        ("owner_id", "=", "user1234"),
        ("size", "=", 2),
        ("coloration", "=", 2),
        ("tag", "=", "family"),
    ],
)

Firestore in modalità Datastore può supportare queste query unendo gli indici incorporati.

Unità di indicizzazione combinate

Firestore in modalità Datastore può utilizzare l'unione di indici quando la query e gli indicisoddisfano tutti i seguenti vincoli:

  • La query utilizza solo filtri di uguaglianza (=)
  • Non esiste un indice composito che corrisponda perfettamente ai filtri e all'ordinamento della query
  • Ogni filtro di uguaglianza corrisponde ad almeno un indice esistente con lo stesso ordine della query

In questo caso, Firestore in modalità Datastore può utilizzare gli indici esistenti per supportare la query anziché richiedere la configurazione di un indice composto aggiuntivo.

Quando due o più indici sono ordinati in base agli stessi criteri, Firestore in modalità Datastore può riunire i risultati di più scansioni dell'indice per trovare quelli comuni a tutti questi indici. Firestore in modalità Datastore può unire gli indici incorporati, poiché tutti ordinano i valori in base alla chiave dell'entità.

Unendo gli indici integrati, Firestore in modalità Datastore supporta le query con filtri di uguaglianza su più proprietà:

Python

from google.cloud import datastore

# For help authenticating your client, visit
# https://cloud.google.com/docs/authentication/getting-started
client = datastore.Client()

query_all_properties = client.query(
    kind="Photo",
    filters=[
        ("owner_id", "=", "user1234"),
        ("size", "=", 2),
        ("coloration", "=", 2),
        ("tag", "=", "family"),
    ],
)

Firestore in modalità Datastore può anche unire i risultati degli indici di più sezioni dello stesso indice. Unendo diverse sezioni dell'indice integrato per la proprietà tag, Firestore in modalità Datastore supporta le query che combinano più filtri tag in un AND logico:

Python

from google.cloud import datastore

# For help authenticating your client, visit
# https://cloud.google.com/docs/authentication/getting-started
client = datastore.Client()

query_tag = client.query(
    kind="Photo",
    filters=[
        ("tag", "=", "family"),
        ("tag", "=", "outside"),
        ("tag", "=", "camping"),
    ],
)

query_owner_size_color_tags = client.query(
    kind="Photo",
    filters=[
        ("owner_id", "=", "user1234"),
        ("size", "=", 2),
        ("coloration", "=", 2),
        ("tag", "=", "family"),
        ("tag", "=", "outside"),
        ("tag", "=", "camping"),
    ],
)

Le query supportate dagli indici integrati uniti completano l'insieme di query richiesto dalla funzionalità di filtro Photo. Tieni presente che il supporto della funzionalità di filtro Photo non ha richiesto indici compositi aggiuntivi.

Quando selezioni gli indici ottimali per la tua app, è importante conoscere la funzionalità di unione degli indici. L'unione degli indici offre a Firestore in modalità Datastore una maggiore flessibilità delle query, ma con un possibile compromesso in termini di prestazioni. La sezione successiva descrive il rendimento dell'unione di indici e come migliorare il rendimento aggiungendo indici compositi.

Trovare l'indice perfetto

L'indice viene ordinato prima per antenato e poi per valori della proprietà, nell'ordine specificato nella definizione dell'indice. L'indice composto perfetto per una query, che consente di eseguire la query in modo più efficiente, è definito sulle seguenti proprietà, in ordine:

  1. Proprietà utilizzate nei filtri di uguaglianza
  2. Proprietà utilizzate negli ordini di ordinamento
  3. Proprietà utilizzate nel filtro distinctOn
  4. Proprietà utilizzate nei filtri di intervallo e di disuguaglianza (non ancora incluse negli ordini di ordinamento)
  5. Proprietà utilizzate nelle aggregazioni e nelle proiezioni (che non sono già incluse negli ordini di ordinamento e nei filtri di intervallo e disuguaglianza)

In questo modo vengono presi in considerazione tutti i risultati per ogni possibile esecuzione della query. I database Firestore in modalità Datastore eseguono una query utilizzando un indice perfetto seguendo i seguenti passaggi:

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

Ad esempio, considera la seguente query:

SELECT * FROM Task
WHERE category = 'Personal'
  AND priority < 3
ORDER BY priority DESC

L'indice composto perfetto per questa query è un indice di chiavi per le entità di tipo Task, con colonne per i valori delle proprietà category e priority. L'indice viene ordinato prima in ordine crescente per category e poi in ordine decrescente per priority:

indexes:
- kind: Task
  properties:
  - name: category
    direction: asc
  - name: priority
    direction: desc

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

SELECT * FROM Task
WHERE category = 'Work'
  AND priority < 5
ORDER BY priority DESC

Per questo indice

indexes:
- kind: Task
  properties:
  - name: category
    direction: asc
  - name: priority
    direction: asc
  - name: created
    direction: asc

L'indice precedente può soddisfare entrambe le seguenti query:

SELECT * FROM Task
WHERE category = 'Personal'
  AND priority = 5
ORDER BY created ASC

e

SELECT * FROM Task
WHERE category = 'Work'
ORDER BY priority ASC, created ASC

Ottimizzazione della selezione dell'indice

Questa sezione descrive le caratteristiche del rendimento dell'unione di indici e due opportunità di ottimizzazione correlate:

  • Aggiungi indici composti per velocizzare le query che si basano su indici uniti
  • Riduci il numero di indici composti sfruttando gli indici uniti

Rendimento dell'unione degli indici

In un'unione di indici, Firestore in modalità Datastore unisce in modo efficiente gli indici utilizzando un algoritmo di join di unione a zig zag. Utilizzando questo algoritmo, la modalità Datastore unisce le potenziali corrispondenze di più scansioni dell'indice per produrre un insieme di risultati che corrisponde a una query. L'unione degli indici combina i componenti del filtro in fase di lettura anziché di scrittura. A differenza della maggior parte delle query Firestore in modalità Datastore, in cui le prestazioni dipendono solo dalle dimensioni del set di risultati, le prestazioni delle query di unione di indici dipendono dai filtri nella query e dal numero di potenziali corrispondenze prese in considerazione dal database.

Il rendimento migliore di un'unione di indici si verifica quando ogni potenziale corrispondenza in un indice soddisfa i filtri delle query. In questo caso, il rendimento è O(R * I) dove R è la dimensione del set di risultati e I è il numero di indici esaminati.

Le prestazioni peggiori si verificano quando il database deve prendere in considerazione molte potenziali corrispondenze, ma solo poche soddisfano i filtri delle query. In questo caso, il rendimento è O(S), dove S è la dimensione dell'insieme più piccolo di potenziali entità di una singola scansione dell'indice.

Il rendimento effettivo dipende dalla forma dei dati. Il numero medio di entità prese in considerazione per ogni risultato restituito è O(S/(R * I)). Il rendimento delle query peggiora quando molte entità corrispondono a ogni scansione dell'indice, ma poche entità corrispondono alla query nel suo complesso, il che significa che R è piccolo e S è grande.

Esistono quattro fattori che riducono questo rischio:

  • Il pianificatore delle query non cerca un'entità finché non sa che l'entità corrisponde all'intera query.

  • L'algoritmo a zig zag non deve trovare tutti i risultati per restituire il risultato successivo. Se richiedi i primi 10 risultati, paghi solo la latenza per trovarli.

  • L'algoritmo a zig zag salta ampie sezioni di risultati di falsi positivi. Il rendimento peggiore si verifica solo se i risultati di falsi positivi sono perfettamente intrecciati (in ordine di ordinamento) tra le scansioni.

  • La latenza dipende dal numero di entità trovate in ogni scansione dell'indice, non dal numero di entità che corrispondono a ciascun filtro. Come mostrato nella sezione successiva, puoi aggiungere indici compositi per migliorare le prestazioni dell'unione di indici.

Accelerare una query di unione di indici

Quando Firestore in modalità Datastore unisce gli indici, ogni scansione dell'indice viene spesso associata a un singolo filtro nella query. Puoi migliorare le prestazioni delle query aggiungendo indici composti che corrispondono a più filtri nella query.

Considera questa query:

Python

from google.cloud import datastore

# For help authenticating your client, visit
# https://cloud.google.com/docs/authentication/getting-started
client = datastore.Client()

query_owner_size_tag = client.query(
    kind="Photo",
    filters=[
        ("owner_id", "=", "username"),
        ("size", "=", 2),
        ("tag", "=", "family"),
    ],
)

Ogni filtro viene mappato a una scansione dell'indice nei seguenti indici integrati:

Index(Photo, owner_id)
Index(Photo, size)
Index(Photo, tag)

Se aggiungi l'indice composito Index(Photo, owner_id, size), la query viene mappata a due scansioni dell'indice anziché a tre:

#  Satisfies both 'owner_id=username' and 'size=2'
Index(Photo, owner_id, size)
Index(Photo, tag)

Prendiamo in considerazione uno scenario con molte immagini grandi, molte immagini in bianco e nero, ma poche immagini panoramiche grandi. Una query che filtra le immagini panoramiche e in bianco e nero sarà lenta se unisce gli indici integrati:

Python

from google.cloud import datastore

# For help authenticating your client, visit
# https://cloud.google.com/docs/authentication/getting-started
client = datastore.Client()

query_size_coloration = client.query(
    kind="Photo", filters=[("size", "=", 2), ("coloration", "=", 1)]
)

Per migliorare le prestazioni delle query, puoi ridurre il valore di S (l'insieme più piccolo di entità in una singola scansione dell'indice) in O(S/(R * I)) aggiungendo il seguente indice composito:

Index(Photo, size, coloration)

Rispetto all'utilizzo di due indici incorporati, questo indice composito genera meno risultati potenziali per gli stessi due filtri di query. Questo approccio migliora notevolmente il rendimento a costo di un indice in più.

Riduzione del numero di indici composti con l'unione di indici

Sebbene gli indici compositi che corrispondono esattamente ai filtri di una query abbiano il rendimento migliore, non è sempre consigliabile o possibile aggiungere un indice composito per ogni combinazione di filtri. Devi bilanciare gli indici composti in base a quanto segue:

  • Limiti degli indici composti:

    Limite Importo
    Numero massimo di indici composti per un database
    Somma massima delle dimensioni delle voci dell'indice composto di un'entità 2 MiB
    Somma massima di quanto segue per un'entità:
    • numero di valori di proprietà indicizzati
    • numero di voci dell'indice composto
    20.000
  • Costi di archiviazione di ogni indice aggiuntivo.
  • Effetti sulla latenza di scrittura.

I problemi di indicizzazione si verificano spesso con i campi con più valori, come la proprietà tag delle entità Photo.

Ad esempio, immagina che la funzionalità di filtro Photo debba ora supportare le clausole di ordinamento decrescente in base a quattro proprietà aggiuntive:

Foto
Proprietà Tipo di valore Descrizione
date_added Numero intero Data/ora
rating Numero in virgola mobile Valutazione complessiva degli utenti
comment_count Numero intero Numero di commenti
download_count Numero intero Numero di download

Se ignori il campo tag, è possibile selezionare gli indici compositi che corrispondono a ogni combinazione di filtri Photo:

Index(Photo, owner_id, -date_added)
Index(Photo, owner_id, -comments)
Index(Photo, size, -date_added)
Index(Photo, size, -comments)
...
Index(Photo, owner_id, size, -date_added)
Index(Photo, owner_id, size, -comments)
...
Index(Photo, owner_id, size, coloration, -date_added)
Index(Photo, owner_id, size, coloration, -comments)

Il numero totale di indici composti è:

2^(number of filters) * (number of different orders) = 2 ^ 3 * 4 = 32 composite indexes

Se provi a supportare fino a 3 filtri tag, il numero totale di indici compositi è:

2 ^ (3 + 3 tag filters) * 4 = 256 indexes.

Anche gli indici che includono proprietà con più valori come tag causano problemi di indici esplosivi che aumentano i costi di archiviazione e la latenza di scrittura.

Per supportare i filtri sul campo tag per questa funzionalità, puoi ridurre il numero totale di indici utilizzando gli indici uniti. Il seguente insieme di indici compositi è il minimo richiesto per supportare la funzionalità di filtro Photo con ordinamento:

Index(Photo, owner_id, -date_added)
Index(Photo, owner_id, -rating)
Index(Photo, owner_id, -comments)
Index(Photo, owner_id, -downloads)
Index(Photo, size, -date_added)
Index(Photo, size, -rating)
Index(Photo, size, -comments)
Index(Photo, size, -downloads)
...
Index(Photo, tag, -date_added)
Index(Photo, tag, -rating)
Index(Photo, tag, -comments)
Index(Photo, tag, -downloads)

Il numero di indici composti definiti è:

(number of filters + 1) * (number of orders) = 7 * 4 = 28

L'unione degli indici offre inoltre i seguenti vantaggi:

  • Consente a un'entità Photo di supportare fino a 1000 tag senza limiti al numero di filtri tag per query.
  • Riduce il numero totale di indici, il che riduce i costi di archiviazione e la latenza di scrittura.

Selezione degli indici per l'app

Puoi selezionare gli indici ottimali per il database in modalità Datastore utilizzando due approcci:

  • Utilizzare l'unione di indici per supportare query aggiuntive

    • Richiede meno indici composti
    • Riduce il costo di archiviazione per entità
    • Migliora la latenza di scrittura
    • Evita indici enormi
    • Il rendimento dipende dalla forma dei dati
  • Definire un indice composto che corrisponda a più filtri in una query

    • Migliora le prestazioni delle query
    • Rendimento delle query coerente che non dipende dalla forma dei dati
    • Deve rimanere al di sotto del limite di indici composti
    • Aumento del costo di archiviazione per entità
    • Aumento della latenza di scrittura

Quando cerchi gli indici ottimali per la tua app, la risposta può cambiare in base alla forma dei dati. Il campionamento delle prestazioni delle query ti offre una buona idea delle query comuni e lente della tua app. Con queste informazioni, puoi aggiungere indici per migliorare le prestazioni delle query comuni e lente.