Plans d'exécution de requêtes

Présentation

Cette page décrit des concepts sur les plans d'exécution de requêtes et leur utilisation par Spanner afin d'exécuter des requêtes dans un environnement distribué. Pour savoir comment afin de récupérer un plan d'exécution pour une requête spécifique à l'aide de dans la console Google Cloud, consultez la page Comprendre le fonctionnement de Spanner requêtes. Vous pouvez également consulter des échantillons de plans de requêtes historiques et comparer les performances d'une requête au fil du temps pour certaines requêtes. Pour en savoir plus, consultez la section Exemples de plans de requêtes échantillonnés.

Spanner utilise des instructions SQL déclaratives pour interroger ses bases de données. Les instructions SQL définissent ce que l'utilisateur souhaite sans préciser comment l'obtenir les résultats. Un plan d'exécution de requêtes réunit l'ensemble des étapes permettant de définir ce "comment". Pour une instruction SQL donnée, les résultats peuvent être obtenus de plusieurs façons. L'optimiseur de requêtes Spanner évalue différents plans d'exécution et choisit celui qu'il considère comme le plus efficace. Spanner utilise ensuite le plan d'exécution pour récupérer les résultats.

Conceptuellement, un plan d'exécution correspond à une arborescence d'opérateurs relationnels. Chaque opérateur lit les lignes à partir de ses entrées et génère des lignes de sortie. Le résultat de l'opérateur situé à la racine de l'exécution est renvoyé en tant que résultat de la requête SQL.

À titre d'exemple, prenons la requête suivante :

SELECT s.SongName FROM Songs AS s;

Elle renvoie un plan d'exécution de requêtes pouvant être visualisé comme suit :

Exemple de plan d'exécution de requêtes

Les requêtes et les plans d'exécution de cette page sont basés sur le schéma de base de données suivant :

CREATE TABLE Singers (
  SingerId   INT64 NOT NULL,
  FirstName  STRING(1024),
  LastName   STRING(1024),
  SingerInfo BYTES(MAX),
  BirthDate  DATE
) PRIMARY KEY(SingerId);

CREATE INDEX SingersByFirstLastName ON Singers(FirstName, LastName);

CREATE TABLE Albums (
  SingerId        INT64 NOT NULL,
  AlbumId         INT64 NOT NULL,
  AlbumTitle      STRING(MAX),
  MarketingBudget INT64
) PRIMARY KEY(SingerId, AlbumId),
  INTERLEAVE IN PARENT Singers ON DELETE CASCADE;

CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle);

CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) STORING (MarketingBudget);

CREATE TABLE Songs (
  SingerId  INT64 NOT NULL,
  AlbumId   INT64 NOT NULL,
  TrackId   INT64 NOT NULL,
  SongName  STRING(MAX),
  Duration  INT64,
  SongGenre STRING(25)
) PRIMARY KEY(SingerId, AlbumId, TrackId),
  INTERLEAVE IN PARENT Albums ON DELETE CASCADE;

CREATE INDEX SongsBySingerAlbumSongNameDesc ON Songs(SingerId, AlbumId, SongName DESC), INTERLEAVE IN Albums;

CREATE INDEX SongsBySongName ON Songs(SongName);

CREATE TABLE Concerts (
  VenueId      INT64 NOT NULL,
  SingerId     INT64 NOT NULL,
  ConcertDate  DATE NOT NULL,
  BeginTime    TIMESTAMP,
  EndTime      TIMESTAMP,
  TicketPrices ARRAY<INT64>
) PRIMARY KEY(VenueId, SingerId, ConcertDate);

Vous pouvez utiliser les instructions LMD (langage de manipulation de données) suivantes pour ajouter des données à ces tables :

INSERT INTO Singers (SingerId, FirstName, LastName, BirthDate)
VALUES (1, "Marc", "Richards", "1970-09-03"),
       (2, "Catalina", "Smith", "1990-08-17"),
       (3, "Alice", "Trentor", "1991-10-02"),
       (4, "Lea", "Martin", "1991-11-09"),
       (5, "David", "Lomond", "1977-01-29");

INSERT INTO Albums (SingerId, AlbumId, AlbumTitle)
VALUES (1, 1, "Total Junk"),
       (1, 2, "Go, Go, Go"),
       (2, 1, "Green"),
       (2, 2, "Forever Hold Your Peace"),
       (2, 3, "Terrified"),
       (3, 1, "Nothing To Do With Me"),
       (4, 1, "Play");

INSERT INTO Songs (SingerId, AlbumId, TrackId, SongName, Duration, SongGenre)
VALUES (2, 1, 1, "Let's Get Back Together", 182, "COUNTRY"),
       (2, 1, 2, "Starting Again", 156, "ROCK"),
       (2, 1, 3, "I Knew You Were Magic", 294, "BLUES"),
       (2, 1, 4, "42", 185, "CLASSICAL"),
       (2, 1, 5, "Blue", 238, "BLUES"),
       (2, 1, 6, "Nothing Is The Same", 303, "BLUES"),
       (2, 1, 7, "The Second Time", 255, "ROCK"),
       (2, 3, 1, "Fight Story", 194, "ROCK"),
       (3, 1, 1, "Not About The Guitar", 278, "BLUES");

Il est difficile d'obtenir des plans d'exécution efficaces, car Spanner divise les données en divisions. Les divisions peuvent bouger indépendamment les unes des autres, et être affectées à différents serveurs, qui peuvent eux-mêmes se trouver dans des emplacements physiques variés. Pour évaluer les plans d'exécution sur les données distribuées, Spanner utilise une exécution basée sur :

  • l'exécution locale des sous-plans dans les serveurs contenant les données ;
  • l'orchestration et l'agrégation de multiples exécutions à distance avec élimination de distributions intensives.

Spanner utilise l'opérateur primitif distributed union, ainsi que ses variantes distributed cross apply et distributed outer apply, pour activer ce modèle.

Plans de requête échantillonnés

Les échantillons de plans de requêtes Spanner vous permettent d'afficher des échantillons d'historiques de plans de requêtes et de comparer les performances d'une requête au fil du temps. Toutes les requêtes disposent d'échantillons de plans de requêtes. Seules les requêtes utilisant une plus grande quantité de processeur peuvent être échantillonnées. La conservation des données pour les exemples de plans de requête Spanner est 30 jours. Vous trouverez des exemples de plans de requêtes sur la page Insights sur les requêtes de la console Google Cloud. Pour obtenir des instructions, consultez Afficher des plans de requête échantillonnés.

La structure d'un plan de requête échantillonné est identique à celle d'une exécution de requête standard du client. Pour en savoir plus sur la façon de comprendre les plans visuels et de les utiliser pour déboguer vos requêtes, consultez la section Visite guidée du visualiseur du plan de requête.

Cas d'utilisation courants des échantillons de plans de requêtes:

Voici quelques cas d'utilisation courants des exemples de plans de requêtes:

Si les performances d'une requête présentent une différence significative au fil du temps ou si vous améliorer les performances d'une requête, consultez les bonnes pratiques SQL pour des instructions de requête optimisées qui aident Spanner à identifier des plans d'exécution efficaces.

Cycle de vie d'une requête

Une requête SQL dans Spanner est d'abord compilée dans un plan d'exécution, puis envoyée à un serveur racine initial pour exécution. Le serveur racine est choisi de manière à réduire le nombre de sauts, et ainsi à atteindre les données interrogées. Le serveur racine :

  • lance l'exécution à distance des sous-plans (si nécessaire) ;
  • attend les résultats des exécutions à distance ;
  • gère toutes les étapes d'exécution locales restantes, telles que l'agrégation des résultats ;
  • renvoie les résultats correspondant à la requête.

Les serveurs distants qui reçoivent un sous-plan agissent comme serveur "racine" de ce sous-plan, suivant ainsi le même modèle que le serveur racine en tête de liste. Le résultat correspond à une arborescence d'exécutions à distance. Sur le plan conceptuel, l'exécution de requêtes s'effectue de haut en bas et les résultats de la requête sont renvoyés de bas en haut. Le schéma suivant illustre ce modèle :

Plan de requête conceptuel

Les exemples ci-dessous illustrent ce modèle plus en détail.

Requêtes d'agrégation

Une requête d'agrégation met en œuvre les requêtes GROUP BY.

Par exemple, si vous utilisez cette requête :

SELECT s.SingerId, COUNT(*) AS SongCount
FROM Songs AS s
WHERE s.SingerId < 100
GROUP BY s.SingerId;

Vous obtenez les résultats suivants :

+----------+-----------+
| SingerId | SongCount |
+----------+-----------+
|        3 |         1 |
|        2 |         8 |
+----------+-----------+

Sur le plan conceptuel, voici le plan d'exécution :

Plan d&#39;exécution de requêtes d&#39;agrégation

Spanner envoie le plan d'exécution à un serveur racine qui coordonne l'exécution de la requête et effectue la distribution à distance des sous-plans.

Ce plan d'exécution commence par une union distribuée, qui distribue des sous-plans aux serveurs distants avec des partitions conformes à SingerId < 100. Après l'analyse Sur les écrans fractionnés individuels, l'opérateur streamaggregate agrège les lignes pour obtenir le décompte de chaque SingerId. L'opérateur Serial result (sérialisation du résultat), sérialise le résultat. Enfin, l'union distribuée combine tous les résultats. et renvoie les résultats de la requête.

Vous pouvez en apprendre davantage sur les agrégats dans la section sur l'opérateur aggregate.

Requêtes de jointure colocalisée

Les tables entrelacées sont stockées physiquement, leurs lignes issues des tables associées étant colocalisées. Les jointures colocalisées permettent de joindre des tables entrelacées. Elles peuvent offrir de meilleures performances que les jointures nécessitant des index ou des jointures ultérieures.

Par exemple, si vous utilisez cette requête :

SELECT al.AlbumTitle, so.SongName
FROM Albums AS al, Songs AS so
WHERE al.SingerId = so.SingerId AND al.AlbumId = so.AlbumId;

(Cette requête suppose que l'objet Songs soit entrelacé dans Albums).

Vous obtenez les résultats suivants :

+-----------------------+--------------------------+
| AlbumTitle            | SongName                 |
+-----------------------+--------------------------+
| Nothing To Do With Me | Not About The Guitar     |
| Green                 | The Second Time          |
| Green                 | Starting Again           |
| Green                 | Nothing Is The Same      |
| Green                 | Let's Get Back Together  |
| Green                 | I Knew You Were Magic    |
| Green                 | Blue                     |
| Green                 | 42                       |
| Terrified             | Fight Story              |
+-----------------------+--------------------------+

Voici le plan d'exécution :

Plan d&#39;exécution de requêtes de jointures colocalisée

Ce plan d'exécution commence par une union distribuée, qui distribue des sous-plans aux serveurs distants contenant des partitions de la table Albums. Comme Songs est une table entrelacée de Albums, chaque serveur distant peut d'exécuter l'ensemble du sous-plan sur chaque serveur distant sans avoir à effectuer de jointure vers un autre serveur.

Les sous-plans contiennent une application croisée. Chaque application croisée effectue une analyse de la table Albums pour récupérer SingerId, AlbumId et AlbumTitle. L'opération cross apply mappe ensuite les résultats de l'analyse de la table sur les résultats à partir d'une analyse d'index portant sur l'index SongsBySingerAlbumSongNameDesc, si elle fait l'objet d'une filtre de SingerId dans l'index correspondant au SingerId du résultat de l'analyse de la table. Chaque application croisée envoie ses résultats à un résultat sérialisé. opérateur qui sérialise les données AlbumTitle et SongName et renvoie aux unions distribuées locales. L'union distribuée agrège les résultats provenant des unions distribuées locales, puis les renvoie en tant que résultats de la requête.

Index et requêtes de jointure ultérieure

L'exemple ci-dessus utilisait une jointure sur deux tables, entrelacées l'une dans l'autre. Les plans d'exécution sont plus complexes et moins efficaces lorsque deux tables (ou une table et un index) ne sont pas entrelacés.

Prenons l'exemple d'un index créé avec la commande suivante :

CREATE INDEX SongsBySongName ON Songs(SongName)

Utilisez cet index dans la requête suivante :

SELECT s.SongName, s.Duration
FROM Songs@{force_index=SongsBySongName} AS s
WHERE STARTS_WITH(s.SongName, "B");

Vous obtenez les résultats suivants :

+----------+----------+
| SongName | Duration |
+----------+----------+
| Blue     |      238 |
+----------+----------+

Voici le plan d'exécution :

Plan d&#39;exécution de requêtes de jointure ultérieure

Le plan d'exécution résultant est compliqué, car l'index SongsBySongName ne contient pas de colonne Duration. Pour obtenir la valeur Duration, Spanner doit effectuer une jointure ultérieure des résultats indexés dans la table. Songs Il s'agit d'une jointure qui n'est pas colocalisée, car la table Songs et l'index global SongsBySongName ne sont pas entrelacés. Le plan d'exécution résultant est plus complexe que l'exemple de jointure colocalisée, car Spanner effectue des optimisations permettant d'accélérer l'exécution si les données ne sont pas colocalisées.

L'opérateur principal est un opérateur distributed cross apply. Ce côté entrée de cet opérateur sont des lots de lignes de l'index SongsBySongName qui respectent le prédicat STARTS_WITH(s.SongName, "B"). L'application croisée distribuée mappe ensuite ces lots avec des serveurs distants, dont les divisions contiennent les données de Duration. Les serveurs distants utilisent une analyse de table pour récupérer la colonne Duration. L'analyse de la table utilise le filtre Condition:($Songs_key_TrackId' = $batched_Songs_key_TrackId), qui joint les TrackId de la table Songs à TrackId des lignes qui ont été regroupées à partir de l'index SongsBySongName.

Les résultats sont agrégés dans la réponse à la requête finale. À son tour, le côté entrée de l'application croisée distribuée contient une paire union distribuée/union distribuée locale pour évaluer les lignes d'index conformes au prédicat STARTS_WITH.

Prenons l'exemple d'une requête légèrement différente ne sélectionnant pas la colonne s.Duration :

SELECT s.SongName
FROM Songs@{force_index=SongsBySongName} AS s
WHERE STARTS_WITH(s.SongName, "B");

Cette requête peut exploiter pleinement l'index, comme indiqué dans le plan d'exécution suivant :

Plan d&#39;exécution de requêtes plus simple

Le plan d'exécution ne nécessite pas de jointure ultérieure, car toutes les colonnes demandées par la requête sont présentes dans l'index.

Étape suivante