Plans d'exécution de requêtes

Présentation

Cette page présente les plans d'exécution de requêtes et la manière dont Spanner les utilise pour effectuer des requêtes dans un environnement distribué. Pour savoir comment récupérer un plan d'exécution pour une requête spécifique à l'aide de la console Google Cloud, consultez la page Comprendre comment Spanner exécute les requêtes. Vous pouvez également afficher des plans de requête historiques échantillonnés et comparer les performances d'une requête au fil du temps pour certaines requêtes. Pour en savoir plus, consultez la section Échantillons de plans de requête.

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 spécifier comment 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ête 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");

L'obtention de plans d'exécution efficaces représente un défi, car Spanner répartit 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 l'exécution en fonction des éléments suivants:

  • 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.

Échantillons de plans de requête

Les plans de requête échantillonnés de Spanner vous permettent d'afficher des exemples de plans de requête historiques et de comparer les performances d'une requête au fil du temps. Un échantillon des plans de requête n'est pas disponible pour toutes les requêtes. Seules les requêtes qui consomment plus de ressources de processeur peuvent être échantillonnées. Les données des exemples de plans de requête Spanner sont conservées pendant 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 échantillons de plans de requête.

L'anatomie d'un plan de requête échantillonné est la même que celle d'un plan d'exécution de requête standard. Pour savoir comment interpréter les plans visuels et les utiliser pour déboguer vos requêtes, consultez la page Visite guidée du visualiseur du plan de requête.

Cas d'utilisation courants des plans de requête échantillonnés:

Voici quelques cas d'utilisation courants des plans de requête échantillonnés:

Si les performances d'une requête présentent des différences significatives au fil du temps ou si vous souhaitez les améliorer, consultez la page Bonnes pratiques SQL pour créer des instructions de requête optimisées qui aident Spanner à trouver 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 à minimiser le nombre de sauts pour atteindre les données interrogées. Ensuite, 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'exécution de requêtes d'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 dont les divisions correspondent à SingerId < 100. Une fois l'analyse effectuée sur chaque écran fractionné, l'opérateur stream agrégé agrège les lignes pour obtenir le nombre total de SingerId. L'opérateur serialize result sérialise ensuite 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'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 divisions de la table Albums. Comme Songs est une table entrelacée de Albums, chaque serveur distant peut exécuter l'intégralité du sous-plan sur chaque serveur distant sans avoir besoin d'effectuer une jointure avec un autre serveur.

Les sous-plans contiennent une application croisée. Chaque application croisée effectue une analyse de table sur la table Albums pour récupérer SingerId, AlbumId et AlbumTitle. L'application croisée mappe ensuite le résultat de l'analyse de table avec celui d'une analyse d'index sur l'index SongsBySingerAlbumSongNameDesc, sous réserve d'un filtre de SingerId dans l'index correspondant au SingerId du résultat de l'analyse de table. Chaque application croisée envoie ses résultats à un opérateur sérialiser le résultat qui sérialise les données AlbumTitle et SongName, puis renvoie les résultats aux unions distribuées locales. L'union distribuée agrège les résultats des unions distribuées locales et les renvoie en tant que résultat 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'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 sur 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 pour accélérer l'exécution si les données ne sont pas colocalisées.

L'opérateur principal est une application croisée distribuée. Le côté entrée de cet opérateur est constitué de 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 Duration. Les serveurs distants utilisent une analyse de table pour récupérer la colonne Duration. L'analyse de table utilise le filtre Condition:($Songs_key_TrackId' = $batched_Songs_key_TrackId), qui joint TrackId de la table Songs à TrackId des lignes traitées par lot à 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'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.

Étapes suivantes