Share via


Conseils relatifs aux performances des kits de développement logiciel (SDK) Azure Cosmos DB

S’APPLIQUE À : NoSQL

Azure Cosmos DB est une base de données distribuée rapide et flexible, qui peut être mise à l’échelle en toute transparence avec des niveaux de latence et de débit garantis. Vous n’avez pas à apporter de modifications d’architecture majeures ou écrire de code complexe pour mettre à l’échelle votre base de données avec Azure Cosmos DB. La réduction et l’augmentation de l’échelle est aussi simple que le passage d’un appel d’API. Pour en savoir plus, consultez Approvisionner le débit sur un conteneur ou Approvisionner le débit sur une base de données.

Réduire les appels au plan de requête

Pour exécuter une requête, il faut générer un plan de requête. En général, celui-ci consiste en une requête réseau adressée à la passerelle Azure Cosmos DB, qui ajoute à la latence de l’opération de requête. Il existe deux façons de supprimer cette requête pour réduire la latence de l’opération de requête :

Optimisation des requêtes à partition unique avec l'exécution directe optimisée

Azure Cosmos DB NoSQL dispose d'une optimisation appelée exécution directe optimisée (ODE), qui peut améliorer l'efficacité de certaines requêtes NoSQL. Plus précisément, les requêtes qui ne nécessitent pas de distribution incluent celles qui peuvent être exécutées sur une seule partition physique ou qui ont des réponses qui ne nécessitent pas de pagination. Les requêtes qui ne nécessitent pas de distribution peuvent ignorer en toute confiance certains processus, tels que la génération de plan de requête côté client et la réécriture des requêtes, ce qui réduit la latence des requêtes et le coût des unités de requête. Si vous spécifiez la clé de partition dans la demande ou la requête elle-même (ou si vous n'avez qu'une seule partition physique) et que les résultats de votre requête ne nécessitent pas de pagination, l'ODE peut améliorer vos requêtes.

Remarque

Ne confondez pas Optimistic Direct Execution (ODE, Exécution directe optimisée), qui offre des performances améliorées pour les requêtes ne nécessitant pas de distribution, avec le Mode direct qui constitue un chemin d’accès pour connecter votre application aux réplicas back-end.

ODE est désormais disponible et activé par défaut dans le Kit de développement logiciel (SDK) .NET 3.38.0 et versions ultérieures. Lorsque vous exécutez une requête et spécifiez une clé de partition dans la requête ou la requête elle-même, ou que votre base de données n’a qu’une seule partition physique, votre exécution de requête peut tirer parti des avantages de l’ODE. Pour désactiver ODE, définissez EnableOptimisticDirectExecution sur false dans QueryRequestOptions.

Les requêtes à partition unique qui comportent des fonctions GROUP BY, ORDER BY, DISTINCT et des fonctions d'agrégation (telles que sum, mean, min et max) peuvent bénéficier de manière significative de l'utilisation de l'ODE. Toutefois, dans les scénarios où la requête vise plusieurs partitions ou nécessite encore une pagination, la latence de la réponse à la requête et le coût de l'EF peuvent être plus élevés que si l'on n'utilisait pas l'ODE. Par conséquent, lors de l’utilisation de l’ODE, nous vous recommandons de :

  • Spécifiez la clé de partition dans l’appel ou la requête proprement dit.
  • Assurez-vous que la taille de vos données n’a pas augmenté et a provoqué le fractionnement de la partition.
  • Assurez-vous que les résultats de votre requête ne nécessitent pas de pagination pour tirer pleinement parti de l’ODE.

Voici quelques exemples de requêtes de partition unique simples qui peuvent tirer parti de l’ODE :

- SELECT * FROM r
- SELECT * FROM r WHERE r.pk == "value"
- SELECT * FROM r WHERE r.id > 5
- SELECT r.id FROM r JOIN id IN r.id
- SELECT TOP 5 r.id FROM r ORDER BY r.id
- SELECT * FROM r WHERE r.id > 5 OFFSET 5 LIMIT 3 

Il peut arriver que des requêtes de partition unique nécessitent toujours une distribution si le nombre d’éléments de données augmente au fil du temps et que votre base de données Azure Cosmos DB fractionne la partition. Voici des exemples de requêtes où cela peut se produire :

- SELECT Count(r.id) AS count_a FROM r
- SELECT DISTINCT r.id FROM r
- SELECT Max(r.a) as min_a FROM r
- SELECT Avg(r.a) as min_a FROM r
- SELECT Sum(r.a) as sum_a FROM r WHERE r.a > 0 

Certaines requêtes complexes peuvent toujours nécessiter une distribution, même si elles ciblent une seule partition. Voici quelques exemples de requêtes simples :

- SELECT Sum(id) as sum_id FROM r JOIN id IN r.id
- SELECT DISTINCT r.id FROM r GROUP BY r.id
- SELECT DISTINCT r.id, Sum(r.id) as sum_a FROM r GROUP BY r.id
- SELECT Count(1) FROM (SELECT DISTINCT r.id FROM root r)
- SELECT Avg(1) AS avg FROM root r 

Il est important de noter que l’ODE peut ne pas toujours récupérer le plan de requête et, par conséquent, n’est pas en mesure d’interdire ou de désactiver les requêtes non prises en charge. Par exemple, après le fractionnement de la partition, ces requêtes ne sont plus éligibles à l’ODE et, par conséquent, ne s’exécutent pas, car l’évaluation du plan de requête côté client les bloque. Pour assurer la compatibilité et la continuité du service, il est essentiel de veiller à ce que seules les requêtes entièrement prises en charge dans les scénarios sans ODE (c'est-à-dire qu'elles s'exécutent et produisent le résultat correct dans le cas général de la multipartition) soient utilisées avec ODE.

Remarque

L’utilisation d’ODE peut potentiellement entraîner la génération d’un nouveau type de jeton de continuation. Un tel jeton n’est par nature pas reconnu par les anciens SDK, ce qui peut entraîner une exception de jeton de continuation incorrecte. Si vous avez un scénario où des jetons générés à partir des kits SDK plus récents sont utilisés par un SKD plus ancien, nous vous recommandons une approche en deux étapes pour la mise à niveau :

  • Effectuez une mise à niveau vers le nouveau Kit de développement logiciel (SDK) et désactivez ODE, les deux ensemble dans le cadre d’un déploiement unique. Attendez la mise à niveau de tous les nœuds.
    • Pour désactiver ODE, définissez EnableOptimisticDirectExecution sur false dans QueryRequestOptions.
  • Activez ODE dans le cadre du deuxième déploiement de tous les nœuds.

Utiliser une génération de plan de requête locale

Le Kit de développement logiciel (SDK) SQL intègre un fichier ServiceInterop.dll natif pour analyser et optimiser les requêtes localement. ServiceInterop.dll est pris en charge uniquement sur la plateforme Windows x64. Les types d’applications suivants utilisent les processus hôte 32 bits par défaut. Pour modifier les processus hôte en traitement 64 bits, procédez comme suit, selon le type de votre application :

  • Pour les applications exécutables, vous pouvez modifier les processus hôte en définissant la plateforme cible sur x64 dans la fenêtre Propriétés du projet, sous l’onglet Générer.

  • Pour les projets basés sur VSTest, vous pouvez modifier les processus hôte en sélectionnant Test>Paramètres de test>Default Processor Architecture as X64 (Définir l’architecture de processeur par défaut sur X64), à partir du menu Visual Studio Test.

  • Pour les applications web ASP.NET déployées localement, vous pouvez modifier les processus hôte en sélectionnant Utiliser la version 64 bits d’IIS Express pour les sites et les projets Web, sous Outils>Options>Projects and Solutions (Projets et solutions)>Projets Web.

  • Pour les applications Web ASP.NET déployées sur Azure, vous pouvez modifier les processus hôte en sélectionnant la plateforme 64 bits dans les paramètres d’application dans le Portail Azure.

Notes

Par défaut, les nouveaux projets Visual Studio sont définis sur Any CPU. Nous vous recommandons de définir votre projet sur x64 afin qu’il ne passe pas à x86. Un projet défini sur Any CPU peut facilement basculer vers x86 si une dépendance est ajoutée à x86 uniquement.
ServiceInterop.dll doit se trouver dans le dossier à partir duquel le fichier DLL du Kit de développement logiciel (SDK) s’exécute. Cela ne doit être un problème que si vous copiez manuellement des DLL ou si vous avez des systèmes de génération/déploiement personnalisés.

Utiliser des requêtes à partition uniques

Pour les requêtes qui ciblent une clé de partition en définissant la propriété PartitionKey dans QueryRequestOptions et ne contiennent aucune agrégation (notamment Distinct, DCount, Group by). Dans cet exemple, le champ de clé de partition de /state est filtré sur la valeur Washington.

using (FeedIterator<MyItem> feedIterator = container.GetItemQueryIterator<MyItem>(
    "SELECT * FROM c WHERE c.city = 'Seattle' AND c.state = 'Washington'"
{
    // ...
}

Vous pouvez également fournir la clé de partition dans le cadre de l’objet des options de requête.

using (FeedIterator<MyItem> feedIterator = container.GetItemQueryIterator<MyItem>(
    "SELECT * FROM c WHERE c.city = 'Seattle'",
    requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey("Washington")}))
{
    // ...
}

Notes

Les requêtes entre partitions requièrent que le Kit de développement logiciel (SDK) visite toutes les partitions existantes pour vérifier les résultats. Plus le conteneur compte de partitions physiques, plus celles-ci risquent d’être lentes.

Éviter de recréer l’itérateur inutilement

Lorsque tous les résultats de la requête sont consommés par le composant actuel, vous n’avez pas besoin de recréer l’itérateur avec la continuation pour chaque page. Préférez toujours le vidage complet de la requête, sauf si la pagination est contrôlée par un autre composant appelant :

using (FeedIterator<MyItem> feedIterator = container.GetItemQueryIterator<MyItem>(
    "SELECT * FROM c WHERE c.city = 'Seattle'",
    requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey("Washington")}))
{
    while (feedIterator.HasMoreResults) 
    {
        foreach(MyItem document in await feedIterator.ReadNextAsync())
        {
            // Iterate through documents
        }
    }
}

Ajuster le degré de parallélisme

Pour les requêtes, réglez la propriété MaxConcurrency dans QueryRequestOptions de manière à identifier les meilleures configurations pour votre application, en particulier si vous effectuez des requêtes sur plusieurs partitions (sans filtre sur la valeur de clé de partition). MaxConcurrency contrôle le nombre maximal de tâches parallèles, c’est-à-dire le nombre maximal de partitions à visiter en parallèle. Définir la valeur sur-1 permet au Kit de développement logiciel (SDK) de décider de la concurrence optimale.

using (FeedIterator<MyItem> feedIterator = container.GetItemQueryIterator<MyItem>(
    "SELECT * FROM c WHERE c.city = 'Seattle'",
    requestOptions: new QueryRequestOptions() { 
        PartitionKey = new PartitionKey("Washington"),
        MaxConcurrency = -1 }))
{
    // ...
}

Supposons que

  • D = nombre maximal par défaut de tâches parallèles (= nombre total de processeurs sur l’ordinateur client)
  • P = nombre maximal de tâches parallèles spécifié par l’utilisateur
  • N = nombre de partitions devant être visitées pour répondre à une requête

Voici comment les requêtes parallèles se comporteraient selon différentes valeurs de P.

  • (P == 0) = > Mode série
  • (P == 1) = > Une tâche maximum
  • (P > 1) = > Nombre minimum de tâches parallèles (P, N)
  • (P < 1) = > Nombre minimum de tâches parallèles (N, D)

Ajuster la taille de page

Quand vous émettez une requête SQL, les résultats retournés sont segmentés si jeu de résultats est trop volumineux.

Notes

La propriété MaxItemCount ne doit pas être utilisée uniquement pour la pagination. Son utilisation principale consiste à améliorer les performances des requêtes en réduisant le nombre maximal d’éléments retournés dans une seule page.

Vous pouvez également définir la taille de la page à l’aide des SDK Azure Cosmos DB disponibles. La propriété MaxItemCount dans QueryRequestOptions vous permet de définir le nombre maximal d’éléments à retourner dans l’opération d’énumération. Lorsque la propriété MaxItemCount est définie sur -1, le Kit de développement logiciel (SDK) recherche automatiquement la valeur optimale en fonction de la taille du document. Par exemple :

using (FeedIterator<MyItem> feedIterator = container.GetItemQueryIterator<MyItem>(
    "SELECT * FROM c WHERE c.city = 'Seattle'",
    requestOptions: new QueryRequestOptions() { 
        PartitionKey = new PartitionKey("Washington"),
        MaxItemCount = 1000}))
{
    // ...
}

Lorsqu’une requête est exécutée, les données qui en résultent sont envoyées dans un paquet TCP. Si vous spécifiez une valeur trop faible pour MaxItemCount, le nombre d’allers-retours requis pour envoyer les données dans le paquet TCP est élevé, ce qui affecte les performances. Par conséquent, si vous ne savez pas quelle valeur définir pour la propriété MaxItemCount, il est préférable d’affecter la valeur -1 et permettre au kit de développement logiciel (SDK) de choisir la valeur par défaut.

Ajuster la taille de mémoire tampon

Une requête parallèle est conçue pour pré-extraire les résultats pendant que le lot de résultats actuel est en cours de traitement par le client. Cette pré-récupération permet d’améliorer la latence globale d’une requête. La propriété MaxBufferedItemCount dans QueryRequestOptions limite le nombre de résultats pré-extraits. Définissez MaxBufferedItemCount sur le nombre attendu de résultats retournés (ou un nombre plus élevé) pour permettre à la requête de recevoir le maximum d’avantages de la pré-récupération (fetch). Si vous définissez cette valeur sur-1, le système détermine automatiquement le nombre d’éléments à mettre en mémoire tampon.

using (FeedIterator<MyItem> feedIterator = container.GetItemQueryIterator<MyItem>(
    "SELECT * FROM c WHERE c.city = 'Seattle'",
    requestOptions: new QueryRequestOptions() { 
        PartitionKey = new PartitionKey("Washington"),
        MaxBufferedItemCount = -1}))
{
    // ...
}

La pré-récupération (fetch) fonctionne de la même façon, quel que soit le degré de parallélisme, et il existe une seule mémoire tampon pour les données de toutes les partitions.

Étapes suivantes

Pour en savoir plus sur les performances liées à l’utilisation du Kit de développement logiciel (SDK) .NET :

Réduire les appels au plan de requête

Pour exécuter une requête, il faut générer un plan de requête. En général, celui-ci consiste en une requête réseau adressée à la passerelle Azure Cosmos DB, qui ajoute à la latence de l’opération de requête.

Utiliser une mise en cache du plan de requête

Le plan de requête, pour une requête étendue à une partition unique, est mis en cache sur le client. Cela élimine la nécessité d’appeler la passerelle pour récupérer le plan de requête après le premier appel. La clé pour le plan de requête en cache est la chaîne de requête SQL. Vous devez vérifier que la requête est paramétrée. Si ce n’est pas le cas, la recherche dans le cache du plan de requête conduira souvent à un échec d’accès au cache, car il est peu probable que la chaîne de requête soit identique d’un appel à l’autre. La mise en cache du plan de requête est activée par défaut pour le SDK Java versions 4.20.0 et ultérieures et pour le SDK Spring Data Azure Cosmos DB versions 3.13.0 et ultérieures.

Utiliser des requêtes de partition uniques paramétrées

Pour les requêtes paramétrées dont l’étendue est limitée à une clé de partition avec setPartitionKey dans CosmosQueryRequestOptions, et qui ne contiennent aucune agrégation (Distinct, DCount, Group by), le plan de requête peut être évité :

CosmosQueryRequestOptions options = new CosmosQueryRequestOptions();
options.setPartitionKey(new PartitionKey("Washington"));

ArrayList<SqlParameter> paramList = new ArrayList<SqlParameter>();
paramList.add(new SqlParameter("@city", "Seattle"));
SqlQuerySpec querySpec = new SqlQuerySpec(
        "SELECT * FROM c WHERE c.city = @city",
        paramList);

//  Sync API
CosmosPagedIterable<MyItem> filteredItems = 
    container.queryItems(querySpec, options, MyItem.class);

//  Async API
CosmosPagedFlux<MyItem> filteredItems = 
    asyncContainer.queryItems(querySpec, options, MyItem.class);

Notes

Les requêtes entre partitions requièrent que le Kit de développement logiciel (SDK) visite toutes les partitions existantes pour vérifier les résultats. Plus le conteneur compte de partitions physiques, plus celles-ci risquent d’être lentes.

Ajuster le degré de parallélisme

Les requêtes parallèles interrogent plusieurs partitions en parallèle. Les données d’un conteneur partitionné individuel sont toutefois extraites en série dans le cadre de la requête. Utilisez donc le paramètre setMaxDegreeOfParallelism sur CosmosQueryRequestOptions pour définir la valeur sur le nombre de partitions que vous avez. Si vous ne connaissez pas le nombre de partitions, vous pouvez utiliser le paramètre setMaxDegreeOfParallelism pour définir un nombre élevé, et le système sélectionne le minimum (nombre de partitions, entrée fournie par l’utilisateur) comme degré maximal de parallélisme. Définir la valeur sur-1 permet au Kit de développement logiciel (SDK) de décider de la concurrence optimale.

Il est important de noter que les requêtes parallèles produisent de meilleurs résultats si les données sont réparties de manière homogène entre toutes les partitions. Si le conteneur est partitionné de telle façon que la totalité ou la majorité des données retournées par une requête sont concentrées dans quelques partitions (une partition dans le pire des cas), les performances de la requête se dégradent.

CosmosQueryRequestOptions options = new CosmosQueryRequestOptions();
options.setPartitionKey(new PartitionKey("Washington"));
options.setMaxDegreeOfParallelism(-1);

// Define the query

//  Sync API
CosmosPagedIterable<MyItem> filteredItems = 
    container.queryItems(querySpec, options, MyItem.class);

//  Async API
CosmosPagedFlux<MyItem> filteredItems = 
    asyncContainer.queryItems(querySpec, options, MyItem.class);

Supposons que

  • D = nombre maximal par défaut de tâches parallèles (= nombre total de processeurs sur l’ordinateur client)
  • P = nombre maximal de tâches parallèles spécifié par l’utilisateur
  • N = nombre de partitions devant être visitées pour répondre à une requête

Voici comment les requêtes parallèles se comporteraient selon différentes valeurs de P.

  • (P == 0) = > Mode série
  • (P == 1) = > Une tâche maximum
  • (P > 1) = > Nombre minimum de tâches parallèles (P, N)
  • (P == -1) => Nombre minimum de tâches parallèles (N, D)

Ajuster la taille de page

Quand vous émettez une requête SQL, les résultats retournés sont segmentés si jeu de résultats est trop volumineux. Par défaut, les résultats sont retournés dans des segments de 100 éléments ou de 4 Mo, selon la limite atteinte en premier. L’augmentation de la taille de la page réduit le nombre d’allers-retours requis et augmente les performances pour les requêtes qui retournent plus de 100 éléments. Si vous ne savez pas quelle valeur définir, 1000 est généralement un bon choix. La consommation de mémoire augmente à mesure que la taille de la page augmente. Par conséquent, si votre charge de travail est sensible à la mémoire, envisagez une valeur inférieure.

Vous pouvez utiliser le paramètre pageSize dans iterableByPage() pour l’API synchrone, et byPage() pour l’API asynchrone pour définir une taille de page :

//  Sync API
Iterable<FeedResponse<MyItem>> filteredItemsAsPages =
    container.queryItems(querySpec, options, MyItem.class).iterableByPage(continuationToken,pageSize);

for (FeedResponse<MyItem> page : filteredItemsAsPages) {
    for (MyItem item : page.getResults()) {
        //...
    }
}

//  Async API
Flux<FeedResponse<MyItem>> filteredItemsAsPages =
    asyncContainer.queryItems(querySpec, options, MyItem.class).byPage(continuationToken,pageSize);

filteredItemsAsPages.map(page -> {
    for (MyItem item : page.getResults()) {
        //...
    }
}).subscribe();

Ajuster la taille de mémoire tampon

Une requête parallèle est conçue pour pré-extraire les résultats pendant que le lot de résultats actuel est en cours de traitement par le client. La pré-extraction permet d’améliorer la latence globale d’une requête. Le paramètre setMaxBufferedItemCount dans CosmosQueryRequestOptions limite le nombre de résultats pré-extraits. Pour optimiser la pré-extraction, définissez le maxBufferedItemCount sur un nombre supérieur à ( pageSize REMARQUE : cela peut également entraîner une consommation de mémoire élevée). Pour réduire le pré-extraction, définissez la maxBufferedItemCount valeur égale à pageSize. Si vous définissez cette valeur sur 0, le système détermine automatiquement le nombre d’éléments à mettre en mémoire tampon.

CosmosQueryRequestOptions options = new CosmosQueryRequestOptions();
options.setPartitionKey(new PartitionKey("Washington"));
options.setMaxBufferedItemCount(-1);

// Define the query

//  Sync API
CosmosPagedIterable<MyItem> filteredItems = 
    container.queryItems(querySpec, options, MyItem.class);

//  Async API
CosmosPagedFlux<MyItem> filteredItems = 
    asyncContainer.queryItems(querySpec, options, MyItem.class);

La pré-récupération (fetch) fonctionne de la même façon, quel que soit le degré de parallélisme, et il existe une seule mémoire tampon pour les données de toutes les partitions.

Étapes suivantes

Pour en savoir plus sur les performances liées à l’utilisation du Kit de développement logiciel (SDK) Java :

Réduire les appels au plan de requête

Pour exécuter une requête, il faut générer un plan de requête. En général, celui-ci consiste en une requête réseau adressée à la passerelle Azure Cosmos DB, qui ajoute à la latence de l’opération de requête. Il est possible de supprimer cette requête et de réduire la latence de l’opération de requête de partition unique. Pour les requêtes à partition unique, spécifiez la valeur de clé de partition pour l’élément et transmettez-la en tant qu’argument partition_key :

items = container.query_items(
        query="SELECT * FROM r where r.city = 'Seattle'",
        partition_key="Washington"
    )

Ajuster la taille de page

Quand vous émettez une requête SQL, les résultats retournés sont segmentés si jeu de résultats est trop volumineux. La propriété max_item_count permet de définir le nombre maximal d’éléments à retourner dans l’opération d’énumération.

items = container.query_items(
        query="SELECT * FROM r where r.city = 'Seattle'",
        partition_key="Washington",
        max_item_count=1000
    )

Étapes suivantes

Pour en savoir plus sur l’utilisation du kit SDK Python pour l’API pour NoSQL :

Réduire les appels au plan de requête

Pour exécuter une requête, il faut générer un plan de requête. En général, celui-ci consiste en une requête réseau adressée à la passerelle Azure Cosmos DB, qui ajoute à la latence de l’opération de requête. Il est possible de supprimer cette requête et de réduire la latence de l’opération de requête de partition unique. Il existe deux manières de procéder pour les requêtes de partition unique qui étendent une requête à une seule partition.

À l’aide d’une expression de requête paramétrable et en spécifiant une clé de partition dans l’instruction de requête. La requête est composée programmatiquement pour SELECT * FROM todo t WHERE t.partitionKey = 'Bikes, Touring Bikes' _:

// find all items with same categoryId (partitionKey)
const querySpec = {
    query: "select * from products p where p.categoryId=@categoryId",
    parameters: [
        {
            name: "@categoryId",
            value: "Bikes, Touring Bikes"
        }
    ]
};

// Get items 
const { resources } = await container.items.query(querySpec).fetchAll();

for (const item of resources) {
    console.log(`${item.id}: ${item.name}, ${item.sku}`);
}

Vous pouvez également spécifier partitionKey dans FeedOptions et transmettre la valeur en tant qu’argument :

const querySpec = {
    query: "select * from products p"
};

const { resources } = await container.items.query(querySpec, { partitionKey: "Bikes, Touring Bikes" }).fetchAll();

for (const item of resources) {
    console.log(`${item.id}: ${item.name}, ${item.sku}`);
}

Ajuster la taille de page

Quand vous émettez une requête SQL, les résultats retournés sont segmentés si jeu de résultats est trop volumineux. La propriété MaxItemCount permet de définir le nombre maximal d’éléments à retourner dans l’opération d’énumération.

const querySpec = {
    query: "select * from products p where p.categoryId=@categoryId",
    parameters: [
        {
            name: "@categoryId",
            value: items[2].categoryId
        }
    ]
};

const { resources } = await container.items.query(querySpec, { maxItemCount: 1000 }).fetchAll();

for (const item of resources) {
    console.log(`${item.id}: ${item.name}, ${item.sku}`);
}

Étapes suivantes

Pour en savoir plus sur l’utilisation du kit SDK Node.js pour l’API pour NoSQL :