Cette page décrit les bonnes pratiques d'utilisation du langage de manipulation de données (LMD) et du LMD partitionné pour les bases de données avec dialecte GoogleSQL et PostgreSQL.
Utiliser une clause WHERE pour réduire la portée des verrous
Les instructions LMD sont exécutées dans les transactions de lecture-écriture. Lorsque Spanner lit des données, il
acquiert des verrous en lecture partagés sur les parties limitées des plages de lignes que vous consultez. Plus précisément, il
n'acquiert ces verrous que sur les colonnes auxquelles vous accédez. Les verrous peuvent inclure des données qui ne
satisfont pas à la condition de filtre de la WHERE clause.
Lorsque Spanner modifie des données à l'aide d'instructions LMD, il acquiert des verrous exclusifs sur les données spécifiques que vous modifiez. De plus, il acquiert des verrous partagés de la même manière que lorsque vous consultez des données. Si votre requête inclut de grandes plages de lignes ou une table entière, les verrous partagés peuvent empêcher les autres transactions de progresser en parallèle.
Pour modifier les données le plus efficacement possible, utilisez une WHERE clause qui permet
à Spanner de ne lire que les lignes nécessaires. Pour ce faire, appliquez un filtre sur la
clé primaire ou sur la clé d'un index secondaire. La clause WHERE limite la portée des
verrous partagés et permet à Spanner de traiter la mise à jour plus efficacement.
Par exemple, imaginons que l'un des musiciens de la table Singers change de
prénom et que vous deviez mettre à jour ce prénom dans votre base de données. Vous pouvez exécuter l'instruction LMD
suivante, mais elle oblige Spanner à analyser l'ensemble de la table et à acquérir des verrous partagés qui
la couvrent dans sa totalité. Par conséquent, Spanner doit lire davantage de données que nécessaire, et
les transactions simultanées ne peuvent pas modifier les données en parallèle :
-- ANTI-PATTERN: SENDING AN UPDATE WITHOUT THE PRIMARY KEY COLUMN
-- IN THE WHERE CLAUSE
UPDATE Singers SET FirstName = "Marcel"
WHERE FirstName = "Marc" AND LastName = "Richards";
Pour optimiser l'opération de mise à jour, incluez la SingerId colonne dans la
WHERE clause. La colonne SingerId est la seule colonne de clé primaire pour
la table Singers :
-- ANTI-PATTERN: SENDING AN UPDATE THAT MUST SCAN THE ENTIRE TABLE
UPDATE Singers SET FirstName = "Marcel"
WHERE FirstName = "Marc" AND LastName = "Richards"
S'il n'existe aucun index sur FirstName ou LastName, vous devez analyser l'intégralité de la table pour trouver les chanteurs cibles. Si vous ne souhaitez pas ajouter d'index secondaire pour optimiser la mise à jour, incluez la colonne SingerId dans la clause WHERE.
La colonne SingerId est la seule colonne de clé primaire pour la table Singers. Pour la trouver, exécutez SELECT dans une transaction en lecture seule distincte avant la transaction de mise à jour :
SELECT SingerId
FROM Singers
WHERE FirstName = "Marc" AND LastName = "Richards"
-- Recommended: Including a seekable filter in the where clause
UPDATE Singers SET FirstName = "Marcel"
WHERE SingerId = 1;
Éviter d'utiliser des instructions LMD et des mutations dans la même transaction
Spanner met en tampon les insertions, les opérations de mise à jour et les suppressions effectuées à l'aide d'instructions LMD côté serveur. Les résultats sont visibles pour les instructions SQL et LMD ultérieures dans la même transaction. Ce processus est différent de celui de l'API Mutation, où Spanner met en tampon les mutations côté client et les envoie côté serveur dans le cadre de l'opération de commit. En conséquence, les mutations dans la requête de commit ne sont pas visibles pour les instructions SQL ou LMD dans la même transaction.
Évitez d'utiliser à la fois des instructions LMD et des mutations dans la même transaction. Si vous utilisez les deux dans la même transaction, vous devez tenir compte de l'ordre d'exécution dans le code de votre bibliothèque cliente. Si une transaction contient à la fois des instructions LMD et des mutations dans la même requête, Spanner exécute les instructions LMD avant les mutations.
Pour les opérations qui ne sont compatibles qu'avec les mutations, vous pouvez
combiner des instructions LMD et des mutations dans la même transaction, par exemple,
insert_or_update.
Si vous utilisez les deux, mettez en tampon les écritures uniquement à la toute fin de la transaction.
Utiliser la fonction PENDING_COMMIT_TIMESTAMP pour écrire des horodatages de commit
GoogleSQL
Pour écrire l'horodatage de commit dans une instruction LMD, utilisez la fonction PENDING_COMMIT_TIMESTAMP. Spanner sélectionne le code temporel de commit lorsque la transaction est validée.
PostgreSQL
Pour écrire l'horodatage de commit dans une instruction LMD, utilisez la fonction SPANNER.PENDING_COMMIT_TIMESTAMP(). Cloud Spanner sélectionne le code temporel de commit lorsque la transaction est validée.
LMD partitionné, et fonctions de date et d'horodatage
Le LMD partitionné utilise une ou plusieurs transactions pouvant être exécutées et enregistrées à des moments différents. Si vous utilisez les fonctions de date ou d'horodatage, les lignes modifiées peuvent contenir des valeurs différentes.
Améliorer la latence avec le LMD par lots
Pour réduire la latence, utilisez le LMD par lots afin d'envoyer plusieurs instructions LMD à Spanner en un seul aller-retour client-serveur.
Le LMD par lots peut appliquer des optimisations à des groupes d'instructions d'un lot pour permettre des mises à jour de données plus rapides et plus efficaces.
Exécuter des écritures avec une seule requête
Spanner optimise automatiquement les groupes contigus d'instructions par lots
INSERT,UPDATEouDELETEsimilaires qui ont des valeurs de paramètres différentes, s'ils ne violent pas les dépendances de données.Prenons l'exemple d'un scénario dans lequel vous souhaitez insérer un grand ensemble de nouvelles lignes dans une table appelée
Albums. Pour permettre à Spanner d'optimiser toutes les instructionsINSERTrequises en une seule action côté serveur efficace, commencez par écrire une instruction LMD appropriée qui utilise des paramètres de requête SQL :INSERT INTO Albums (SingerId, AlbumId, AlbumTitle) VALUES (@Singer, @Album, @Title);Envoyez ensuite à Spanner un lot LMD qui appelle cette instruction de manière répétée et contiguë, les répétitions ne différant que par les valeurs que vous liez aux trois paramètres de requête de l'instruction. Spanner optimise ces instructions LMD structurellement identiques en une seule opération côté serveur avant de l'exécuter.
Exécuter des écritures en parallèle
Spanner optimise automatiquement les groupes contigus d'instructions LMD en les exécutant en parallèle lorsque cela ne viole pas les dépendances de données. Cette optimisation améliore les performances d'un plus grand ensemble d'instructions LMD par lots, car elle peut s'appliquer à une combinaison de types d'instruction LMD (
INSERT,UPDATEetDELETE) et aux instructions LMD paramétrées ou non paramétrées.Par exemple, notre schéma d'exemple comporte les tables
Singers,AlbumsetAccounts.Albumsest entrelacée dansSingerset stocke des informations sur les albums deSingers. Le groupe d'instructions contigu suivant écrit de nouvelles lignes dans plusieurs tables et ne présente pas de dépendances de données complexes.INSERT INTO Singers (SingerId, Name) VALUES(1, "John Doe"); INSERT INTO Singers (SingerId, Name) VALUES(2, "Marcel Richards"); INSERT INTO Albums(SingerId, AlbumId, AlbumTitle) VALUES (1, 10001, "Album 1"); INSERT INTO Albums(SingerId, AlbumId, AlbumTitle) VALUES (1, 10002, "Album 2"); INSERT INTO Albums(SingerId, AlbumId, AlbumTitle) VALUES (2, 10001, "Album 1"); UPDATE Accounts SET Balance = 100 WHERE AccountId = @AccountId;Spanner optimise ce groupe d'instructions LMD en les exécutant en parallèle. Les écritures sont appliquées dans l'ordre des instructions du lot et conservent la sémantique du LMD par lots si une instruction échoue lors de l'exécution.
Activer le traitement par lots côté client dans JDBC
Pour les applications Java qui utilisent un pilote
JDBC compatible avec Spanner, vous pouvez réduire la latence en activant le traitement par lots LMD
côté client. Le pilote JDBC possède une
propriété de connexion
appelée auto_batch_dml qui, lorsqu'elle est activée, met en tampon les instructions LMD sur le client
et les envoie à Spanner en tant que lot unique. Cela peut réduire le nombre d'allers-retours vers le serveur et améliorer les performances globales.
Par défaut, auto_batch_dml est défini sur false. Vous pouvez l'activer en le définissant sur true dans votre chaîne de connexion JDBC.
Exemple :
String url = "jdbc:cloudspanner:/projects/my-project/instances/my-instance/databases/my-database;auto_batch_dml=true";
try (Connection connection = DriverManager.getConnection(url)) {
// Include your DML statements for batching here
}
Lorsque cette propriété de connexion est activée, Spanner envoie les instructions LMD mises en tampon sous forme de lot lorsqu'une instruction non LMD est exécutée ou lorsque la transaction en cours est validée. Cette propriété ne s'applique qu'aux transactions de lecture-écriture. Les instructions LMD en mode autocommit sont exécutées directement.
Par défaut, le nombre de mises à jour pour les instructions LMD mises en tampon est défini sur 1. Vous pouvez modifier cette valeur en définissant la variable de connexion auto_batch_dml_update_count sur une autre valeur. Pour en savoir plus, consultez
Propriétés de connexion compatibles avec JDBC.
Utiliser l'option last_statement pour réduire la latence du LMD
Lorsque la dernière instruction d'une transaction de lecture-écriture est une instruction LMD, vous pouvez utiliser l'option de requête last_statement pour réduire la latence. Cette option est
disponible dans les
executeSql
et executeStreamingSql
API de requête.
L'utilisation de cette option diffère certaines étapes de validation, telles que la validation des contraintes uniques, jusqu'à la validation de la transaction. Lorsque vous utilisez last_statement, les opérations suivantes, telles que les lectures, les requêtes et le LMD, dans la même transaction sont rejetées. Cette option n'est pas compatible avec les mutations. Si vous incluez des mutations dans la même transaction, Spanner renvoie une erreur.
L'option last_statement est compatible avec les bibliothèques clientes suivantes :
- Go version 1.77.0 ou ultérieure
- Java version 2.27.0 ou ultérieure
- Python version 3.53.0 ou ultérieure
- PGAdapter version 0.45.0 ou ultérieure
Elle est compatible et activée par défaut lorsque vous utilisez le mode autocommit dans les pilotes suivants :
- Pilote JDBC version 6.87.0 ou ultérieure
- Pilote Go database/sql version 1.11.2 ou ultérieure
Pilote Python dbapi version 3.53.0 ou ultérieure
Go
GoogleSQL
import (
"context"
"fmt"
"io"
"cloud.google.com/go/spanner"
)
// Updates a row while also setting the update DML as the last
// statement.
func updateDmlWithLastStatement(w io.Writer, db string) error {
ctx := context.Background()
client, err := spanner.NewClient(ctx, db)
if err != nil {
return err
}
defer client.Close()
_, err = client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
// other statements for the transaction if any.
updateStmt := spanner.Statement{
SQL: `UPDATE Singers SET LastName = 'Doe' WHERE SingerId = 54213`,
}
opts := spanner.QueryOptions{LastStatement: true}
updateRowCount, err := txn.UpdateWithOptions(ctx, updateStmt, opts)
if err != nil {
return err
}
fmt.Fprintf(w, "%d record(s) updated.\n", updateRowCount)
return nil
})
if err != nil {
return err
}
return nil
}
PostgreSQL
import (
"context"
"fmt"
"io"
"cloud.google.com/go/spanner"
)
// Updates a row while also setting the update DML as the last
// statement.
func pgUpdateDmlWithLastStatement(w io.Writer, db string) error {
ctx := context.Background()
client, err := spanner.NewClient(ctx, db)
if err != nil {
return err
}
defer client.Close()
_, err = client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
// other statements for the transaction if any.
updateStmt := spanner.Statement{
SQL: `UPDATE Singers SET LastName = 'Doe' WHERE SingerId = 54214`,
}
opts := spanner.QueryOptions{LastStatement: true}
updateRowCount, err := txn.UpdateWithOptions(ctx, updateStmt, opts)
if err != nil {
return err
}
fmt.Fprintf(w, "%d record(s) updated.\n", updateRowCount)
return nil
})
if err != nil {
return err
}
return nil
}
Java
GoogleSQL
static void UpdateUsingLastStatement(DatabaseClient client) {
client
.readWriteTransaction()
.run(
transaction -> {
// other statements for the transaction if any
// Pass in the `lastStatement` option to the last DML statement of the transaction.
transaction.executeUpdate(
Statement.of(
"UPDATE Singers SET Singers.LastName = 'Doe' WHERE SingerId = 54213\n"),
Options.lastStatement());
System.out.println("Singer last name updated.");
return null;
});
}
PostgreSQL
static void UpdateUsingLastStatement(DatabaseClient client) {
client
.readWriteTransaction()
.run(
transaction -> {
// other statements for the transaction if any.
// Pass in the `lastStatement` option to the last DML statement of the transaction.
transaction.executeUpdate(
Statement.of("UPDATE Singers SET LastName = 'Doe' WHERE SingerId = 54214\n"),
Options.lastStatement());
System.out.println("Singer last name updated.");
return null;
});
}
Python
GoogleSQL
def dml_last_statement_option(instance_id, database_id):
"""Updates using DML where the update set the last statement option."""
# [START spanner_dml_last_statement]
# instance_id = "your-spanner-instance"
# database_id = "your-spanner-db-id"
spanner_client = spanner.Client()
instance = spanner_client.instance(instance_id)
database = instance.database(database_id)
def update_singers(transaction):
# other statements for the transaction if any.
update_row_ct = transaction.execute_update(
"UPDATE Singers SET LastName = 'Doe' WHERE SingerId = 54213",
last_statement=True)
print("{} record(s) updated.".format(update_row_ct))
database.run_in_transaction(update_singers)
PostgreSQL
def dml_last_statement_option(instance_id, database_id):
"""Updates using DML where the update set the last statement option."""
# instance_id = "your-spanner-instance"
# database_id = "your-spanner-db-id"
spanner_client = spanner.Client()
instance = spanner_client.instance(instance_id)
database = instance.database(database_id)
def update_singers(transaction):
# other statements for the transaction if any.
update_row_ct = transaction.execute_update(
"UPDATE Singers SET LastName = 'Doe' WHERE SingerId = 54214",
last_statement=True)
print("{} record(s) updated.".format(update_row_ct))
database.run_in_transaction(update_singers)