Partager via


TripPin, partie 10 - Query Folding de base

Remarque

Ce contenu fait actuellement référence à celui d’une implémentation héritée pour les journaux d’activité dans Visual Studio. Le contenu sera mis à jour dans un futur proche pour couvrir le nouveau Kit de développement logiciel (SDK) Power Query dans Visual Studio Code.

Ce tutoriel en plusieurs parties traite de la création d’une extension de source de données pour Power Query. Le tutoriel est destiné à être utilisé de manière séquentielle : chaque leçon s’appuie sur le connecteur créé dans les leçons précédentes, ajoutant de nouvelles fonctionnalités de manière incrémentielle.

Dans cette leçon, vous allez :

  • Découvrir les principes de base du Query Folding
  • Découvrir la fonction Table.View
  • Répliquer les gestionnaires de Query Folding OData pour :
  • $top
  • $skip
  • $count
  • $select
  • $orderby

L’une des fonctionnalités les plus puissantes du langage M est sa capacité à pousser le travail de transformation vers une ou plusieurs sources de données sous-jacentes. On parle de Query Folding (d’autres outils/technologies offrent une fonction similaire appelée « Predicate Pushdown » ou « Query Delegation »).

Lors de la création d’un connecteur personnalisé qui utilise une fonction M avec Query Folding intégré, comme OData.Feed ou Odbc.DataSource, votre connecteur hérite automatiquement et gratuitement de cette fonctionnalité.

Ce tutoriel réplique le comportement de Query Folding intégré pour OData en implémentant des gestionnaires de fonctions pour la fonction Table.View. Cette partie du tutoriel implémente les gestionnaires les plus faciles à implémenter (c’est-à-dire ceux qui ne nécessitent pas d’analyse d’expression ni de suivi d’état).

Pour en savoir plus sur les fonctionnalités liées aux requêtes qu’un service OData peut offrir, accédez aux conventions des URL OData v4.

Remarque

Comme indiqué précédemment, la fonction OData.Feed fournit automatiquement des capacités de Query Folding. Étant donné que la série TripPin traite le service OData comme une API REST régulière, en utilisant Web.Contents plutôt que OData.Feed, vous devez implémenter vous-même les gestionnaires de Query Folding. Pour une utilisation dans le monde réel, nous vous recommandons d’utiliser OData.Feed dans la mesure du possible.

Accédez à Vue d'ensemble de l’évaluation de requête et de Query Folding dans Power Query pour en savoir plus sur Query Folding.

Utilisation de Table.View

La fonction Table.View permet à un connecteur personnalisé de remplacer les gestionnaires de transformation par défaut pour votre source de données. Une implémentation de Table.View fournit une fonction pour un ou plusieurs des gestionnaires pris en charge. Si un gestionnaire n’est pas implémenté ou retourne error durant l’évaluation, le moteur M revient à son gestionnaire par défaut.

Quand un connecteur personnalisé utilise une fonction qui ne prend pas en charge le Query Folding implicite, comme Web.Contents, les gestionnaires de transformation par défaut sont toujours exécutés localement. Si l’API REST à laquelle vous vous connectez prend en charge les paramètres de requête dans le cadre de la requête, Table.View vous permet d’ajouter des optimisations qui permettent de pousser le travail de transformation vers le service.

La fonction Table.View a la signature suivante :

Table.View(table as nullable table, handlers as record) as table

Votre implémentation enveloppe votre fonction principale de source de données. Deux gestionnaires sont requis pour Table.View :

  • GetType : retourne le table type attendu du résultat de la requête
  • GetRows : retourne le résultat table réel de votre fonction de source de données

L’implémentation la plus simple est similaire à l’exemple suivant :

TripPin.SuperSimpleView = (url as text, entity as text) as table =>
    Table.View(null, [
        GetType = () => Value.Type(GetRows()),
        GetRows = () => GetEntity(url, entity)
    ]);

Mettez à jour la fonction TripPinNavTable pour appeler TripPin.SuperSimpleView plutôt que GetEntity :

withData = Table.AddColumn(rename, "Data", each TripPin.SuperSimpleView(url, [Name]), type table),

Si vous réexécutez les tests unitaires, vous voyez que le comportement de votre fonction n’est pas modifié. Dans ce cas, votre implémentation Table.View passe simplement par l’appel à GetEntity. Étant donné que vous n’avez pas (encore) implémenté de gestionnaires de transformation, le paramètre url d’origine reste inchangé.

Implémentation initiale de Table.View

L’implémentation ci-dessus de Table.View est simple, mais pas très utile. L’implémentation suivante est utilisée comme base de référence : elle n’implémente aucune fonctionnalité de repliage, mais offre la génération de modèles automatique dont vous aurez besoin.

TripPin.View = (baseUrl as text, entity as text) as table =>
    let
        // Implementation of Table.View handlers.
        //
        // We wrap the record with Diagnostics.WrapHandlers() to get some automatic
        // tracing if a handler returns an error.
        //
        View = (state as record) => Table.View(null, Diagnostics.WrapHandlers([
            // Returns the table type returned by GetRows()
            GetType = () => CalculateSchema(state),

            // Called last - retrieves the data from the calculated URL
            GetRows = () => 
                let
                    finalSchema = CalculateSchema(state),
                    finalUrl = CalculateUrl(state),

                    result = TripPin.Feed(finalUrl, finalSchema),
                    appliedType = Table.ChangeType(result, finalSchema)
                in
                    appliedType,

            //
            // Helper functions
            //
            // Retrieves the cached schema. If this is the first call
            // to CalculateSchema, the table type is calculated based on
            // the entity name that was passed into the function.
            CalculateSchema = (state) as type =>
                if (state[Schema]? = null) then
                    GetSchemaForEntity(entity)
                else
                    state[Schema],

            // Calculates the final URL based on the current state.
            CalculateUrl = (state) as text => 
                let
                    urlWithEntity = Uri.Combine(state[Url], state[Entity])
                in
                    urlWithEntity
        ]))
    in
        View([Url = baseUrl, Entity = entity]);

Si vous regardez l’appel à Table.View, vous voyez une fonction de wrapper supplémentaire autour de l'enregistrement handlersDiagnostics.WrapHandlers. Cette fonction d’assistance se trouve dans le module Diagnostics (qui a été présenté dans la leçon sur l'ajout de diagnostics), et vous offre un moyen utile de suivre automatiquement toutes les erreurs générées par des gestionnaires individuels.

Les fonctions GetType et GetRows ont été mises à jour pour utiliser deux nouvelles fonctions d’assistance : CalculateSchema et CalculateUrl. À l’heure actuelle, les implémentations de ces fonctions sont assez simples ; remarquez que certaines parties correspondent à ce que faisait précédemment la fonction GetEntity.

Enfin, remarquez que vous définissez une fonction interne (View) qui accepte un paramètre state. Quand vous implémentez d’autres gestionnaires, ceux-ci appellent de manière récursive la fonction interne View, entraînant au fur et à mesure la mise à jour et le passage de state.

Mettez à jour la fonction TripPinNavTable une nouvelle fois, en remplaçant l’appel à TripPin.SuperSimpleView par un appel à la nouvelle fonction TripPin.View, puis réexécutez les tests unitaires. Vous ne verrez pas encore de nouvelles fonctionnalités, mais vous disposez maintenant d’une base de référence solide pour les tests.

Implémentation du query folding

Étant donné que le moteur M revient automatiquement au traitement local quand une requête ne peut pas être repliée, vous devez effectuer quelques étapes supplémentaires pour vérifier que vos gestionnaires Table.View fonctionnent correctement.

Pour valider manuellement le comportement de pliage, examinez les demandes d’URL de vos tests unitaires avec un outil comme Fiddler. Alternativement, la journalisation de diagnostic que vous avez ajoutée à TripPin.Feed émet l’URL complète en cours d’exécution, qui doit inclure les paramètres de chaîne de requête OData que vos gestionnaires ajoutent.

Un moyen automatisé de valider le Query Folding consiste à forcer l’exécution de votre test unitaire à échouer si une requête ne se plie pas entièrement. Pour ce faire, ouvrez les propriétés du projet et définissez Erreur en cas d’échec de pliage sur True. Quand ce paramètre est activé, toute requête nécessitant un traitement local entraîne l’erreur suivante :

Nous n’avons pas pu plier l’expression sur la source de données. Essayez une expression plus simple.

Vous pouvez tester cela en ajoutant un nouveau Fact à votre fichier de test unitaire qui contient une ou plusieurs transformations de table.

// Query folding tests
Fact("Fold $top 1 on Airlines", 
    #table( type table [AirlineCode = text, Name = text] , {{"AA", "American Airlines"}} ), 
    Table.FirstN(Airlines, 1)
)

Remarque

Le paramètre Erreur en cas d’échec de pliage est une approche « tout ou rien ». Si vous souhaitez tester des requêtes qui ne sont pas conçues pour être pliées dans le cadre de vos tests unitaires, vous devez ajouter une logique conditionnelle pour activer/désactiver les tests en conséquence.

Dans chaque section restante de ce tutoriel, un nouveau gestionnaire Table.View est ajouté. Vous adoptez une approche de développement piloté par les tests (TDD, Test Driven Development) dans laquelle vous commencerez par ajouter des tests unitaires défaillants, puis implémenterez le code M pour les résoudre.

Les sections de gestionnaire ci-dessous décrivent les fonctionnalités fournies par le gestionnaire, la syntaxe de requête équivalente OData, les tests unitaires et l’implémentation. À l’aide du code de génération de modèles automatique décrit ci-dessus, chaque implémentation de gestionnaire nécessite deux modifications :

  • Ajout du gestionnaire à Table.View qui mettra à jour l’enregistrement state.
  • Modification de CalculateUrl pour récupérer les valeurs de state et les ajouter aux paramètres d’URL et/ou de chaîne de requête.

Gestion de Table.FirstN avec OnTake

Le gestionnaire OnTake reçoit un paramètre count, qui est le nombre maximum de lignes à prendre de GetRows. En termes OData, vous pouvez traduire cela en paramètre de requête $top.

Vous utilisez les tests unitaires suivants :

// Query folding tests
Fact("Fold $top 1 on Airlines", 
    #table( type table [AirlineCode = text, Name = text] , {{"AA", "American Airlines"}} ), 
    Table.FirstN(Airlines, 1)
),
Fact("Fold $top 0 on Airports", 
    #table( type table [Name = text, IataCode = text, Location = record] , {} ), 
    Table.FirstN(Airports, 0)
),

Ces deux tests utilisent Table.FirstN pour filtrer le jeu de résultats sur les X premières lignes. Si vous exécutez ces tests avec Erreur en cas d’échec de pliage défini sur False (valeur par défaut), les tests doivent réussir. En revanche, si vous exécutez Fiddler (ou vérifiez les journaux de trace), vous remarquez que la requête que vous envoyez ne contient aucun paramètre de requête OData.

Trace de diagnostic.

Si vous définissez Erreur en cas d’échec de pliage sur True, les tests échouent avec l’erreur Please try a simpler expression.. Pour corriger cette erreur, vous définissez votre premier gestionnaire Table.View pour OnTake.

Le gestionnaire OnTake ressemble au code suivant :

OnTake = (count as number) =>
    let
        // Add a record with Top defined to our state
        newState = state & [ Top = count ]
    in
        @View(newState),

La fonction CalculateUrl est mise à jour pour extraire la valeur Top de l’enregistrement state et définir le bon paramètre dans la chaîne de requête.

// Calculates the final URL based on the current state.
CalculateUrl = (state) as text => 
    let
        urlWithEntity = Uri.Combine(state[Url], state[Entity]),

        // Uri.BuildQueryString requires that all field values
        // are text literals.
        defaultQueryString = [],

        // Check for Top defined in our state
        qsWithTop =
            if (state[Top]? <> null) then
                // add a $top field to the query string record
                defaultQueryString & [ #"$top" = Number.ToText(state[Top]) ]
            else
                defaultQueryString,

        encodedQueryString = Uri.BuildQueryString(qsWithTop),
        finalUrl = urlWithEntity & "?" & encodedQueryString
    in
        finalUrl

En réexécutant les tests unitaires, vous remarquez que l’URL à laquelle vous accédez contient maintenant le paramètre $top. En raison de l'encodage d'URL, $top apparaît sous la forme de %24top, mais le service OData est suffisamment intelligent pour le convertir automatiquement.

Trace de diagnostic avec top.

Gestion de Table.Skip avec OnSkip

Le gestionnaire OnSkip ressemble beaucoup à OnTake. Il reçoit un paramètre count, qui est le nombre de lignes à ignorer du jeu de résultats. La conversion de ce gestionnaire est bien effectuée vers le paramètre de requête OData $skip.

Tests unitaires :

// OnSkip
Fact("Fold $skip 14 on Airlines",
    #table( type table [AirlineCode = text, Name = text] , {{"EK", "Emirates"}} ), 
    Table.Skip(Airlines, 14)
),
Fact("Fold $skip 0 and $top 1",
    #table( type table [AirlineCode = text, Name = text] , {{"AA", "American Airlines"}} ),
    Table.FirstN(Table.Skip(Airlines, 0), 1)
),

Implémentation :

// OnSkip - handles the Table.Skip transform.
// The count value should be >= 0.
OnSkip = (count as number) =>
    let
        newState = state & [ Skip = count ]
    in
        @View(newState),

Mises à jour correspondantes à CalculateUrl :

qsWithSkip = 
    if (state[Skip]? <> null) then
        qsWithTop & [ #"$skip" = Number.ToText(state[Skip]) ]
    else
        qsWithTop,

Plus d’informations : Table.Skip

Gestion de Table.SelectColumns avec OnSelectColumns

Le gestionnaire OnSelectColumns est appelé quand l’utilisateur sélectionne ou supprime des colonnes du jeu de résultats. Le gestionnaire reçoit un list de text valeurs, représentant la ou les colonnes à sélectionner.

En termes OData, cette opération correspond à l’option de requête $select.

L’avantage du repliage d’une sélection de colonnes devient évident quand vous traitez des tables avec de nombreuses colonnes. L’opérateur $select supprime les colonnes non sélectionnées du jeu de résultats, ce qui donne des requêtes plus efficaces.

Tests unitaires :

// OnSelectColumns
Fact("Fold $select single column", 
    #table( type table [AirlineCode = text] , {{"AA"}} ),
    Table.FirstN(Table.SelectColumns(Airlines, {"AirlineCode"}), 1)
),
Fact("Fold $select multiple column", 
    #table( type table [UserName = text, FirstName = text, LastName = text],{{"russellwhyte", "Russell", "Whyte"}}), 
    Table.FirstN(Table.SelectColumns(People, {"UserName", "FirstName", "LastName"}), 1)
),
Fact("Fold $select with ignore column", 
    #table( type table [AirlineCode = text] , {{"AA"}} ),
    Table.FirstN(Table.SelectColumns(Airlines, {"AirlineCode", "DoesNotExist"}, MissingField.Ignore), 1)
),

Les deux premiers tests sélectionnent différents nombres de colonnes avec Table.SelectColumns, et incluent un appel à Table.FirstN pour simplifier le cas de test.

Remarque

Si le test retournait simplement les noms des colonnes (en utilisant Table.ColumnNames et non des données), la demande au service OData ne serait jamais réellement envoyée. Cela est dû au fait que l’appel à GetType retourne le schéma qui contient toutes les informations dont le moteur M a besoin pour calculer le résultat.

Le troisième test utilise l’option MissingField.Ignore, qui indique au moteur M d’ignorer toutes les colonnes sélectionnées qui n’existent pas dans le jeu de résultats. Le gestionnaire OnSelectColumns n’a pas à se préoccuper de cette option. Le moteur M la gère automatiquement (c’est-à-dire que les colonnes manquantes ne seront pas incluses dans la liste columns).

Remarque

L’autre option pour Table.SelectColumns, MissingField.UseNull, nécessite un connecteur pour implémenter le gestionnaire OnAddColumn. Cette opération sera effectuée dans une leçon suivante.

L’implémentation pour OnSelectColumns fait deux choses :

  • Ajoute la liste des colonnes sélectionnées à state.
  • Recalcule la valeur Schema afin que vous puissiez définir le type de table approprié.
OnSelectColumns = (columns as list) =>
    let
        // get the current schema
        currentSchema = CalculateSchema(state),
        // get the columns from the current schema (which is an M Type value)
        rowRecordType = Type.RecordFields(Type.TableRow(currentSchema)),
        existingColumns = Record.FieldNames(rowRecordType),
        // calculate the new schema
        columnsToRemove = List.Difference(existingColumns, columns),
        updatedColumns = Record.RemoveFields(rowRecordType, columnsToRemove),
        newSchema = type table (Type.ForRecord(updatedColumns, false))
    in
        @View(state & 
            [ 
                SelectColumns = columns,
                Schema = newSchema
            ]
        ),

CalculateUrl est mis à jour pour récupérer la liste des colonnes de l’état et les combiner (avec un séparateur) pour le paramètre $select.

// Check for explicitly selected columns
qsWithSelect =
    if (state[SelectColumns]? <> null) then
        qsWithSkip & [ #"$select" = Text.Combine(state[SelectColumns], ",") ]
    else
        qsWithSkip,

Gestion de Table.Sort avec OnSort

Le gestionnaire OnSort reçoit une liste d’enregistrements de type :

type [ Name = text, Order = Int16.Type ]

Chaque enregistrement contient un champ Name, indiquant le nom de la colonne, et un champ Order égal à Order.Ascending ou Order.Descending.

En termes OData, cette opération correspond à l’option de requête $orderby. La syntaxe $orderby a le nom de la colonne suivi de asc ou de desc pour indiquer l’ordre croissant ou décroissant. En cas de tri sur plusieurs colonnes, les valeurs sont séparées par une virgule. Si le paramètre columns contient plusieurs éléments, il est important de conserver l’ordre dans lequel ils apparaissent.

Tests unitaires :

// OnSort
Fact("Fold $orderby single column",
    #table( type table [AirlineCode = text, Name = text], {{"TK", "Turkish Airlines"}}),
    Table.FirstN(Table.Sort(Airlines, {{"AirlineCode", Order.Descending}}), 1)
),
Fact("Fold $orderby multiple column",
    #table( type table [UserName = text], {{"javieralfred"}}),
    Table.SelectColumns(Table.FirstN(Table.Sort(People, {{"LastName", Order.Ascending}, {"UserName", Order.Descending}}), 1), {"UserName"})
)

Implémentation :

// OnSort - receives a list of records containing two fields: 
//    [Name]  - the name of the column to sort on
//    [Order] - equal to Order.Ascending or Order.Descending
// If there are multiple records, the sort order must be maintained.
//
// OData allows you to sort on columns that do not appear in the result
// set, so we do not have to validate that the sorted columns are in our 
// existing schema.
OnSort = (order as list) =>
    let
        // This will convert the list of records to a list of text,
        // where each entry is "<columnName> <asc|desc>"
        sorting = List.Transform(order, (o) => 
            let
                column = o[Name],
                order = o[Order],
                orderText = if (order = Order.Ascending) then "asc" else "desc"
            in
                column & " " & orderText
        ),
        orderBy = Text.Combine(sorting, ", ")
    in
        @View(state & [ OrderBy = orderBy ]),

Mises à jour apportées à CalculateUrl :

qsWithOrderBy = 
    if (state[OrderBy]? <> null) then
        qsWithSelect & [ #"$orderby" = state[OrderBy] ]
    else
        qsWithSelect,

Gestion de Table.RowCount avec GetRowCount

Contrairement aux autres gestionnaires de requêtes que vous implémentez, le gestionnaire GetRowCount retourne une valeur unique, à savoir le nombre de lignes attendues dans le jeu de résultats. Dans une requête M, cette valeur est généralement le résultat de la transformation de Table.RowCount.

Plusieurs options s’offrent à vous pour gérer cette valeur dans le cadre d’une requête OData :

L’inconvénient de l’approche du paramètre de requête est que vous devez toujours envoyer la requête entière au service OData. Étant donné que le nombre revient en ligne dans le cadre du jeu de résultats, vous devez traiter la première page de données du jeu de résultats. Bien que ce processus soit plus efficace que de lire l’ensemble des résultats et de compter les lignes, vous vous retrouvez tout de même avec une charge de travail importante.

L’avantage de l’approche du segment de tracé est que vous ne recevez qu’une seule valeur scalaire dans le résultat. Cette approche rend l’ensemble de l’opération beaucoup plus efficace. Toutefois, comme décrit dans la spécification OData, le segment de tracé /$count retourne une erreur si vous incluez d’autres paramètres de requête, comme $top ou $skip, ce qui limite son utilité.

Dans ce didacticiel, vous avez implémenté le gestionnaire GetRowCount en suivant l’approche du segment de tracé. Pour éviter les erreurs générées si d’autres paramètres de requête sont inclus, vous avez vérifié les autres valeurs d’état et retourné une « erreur non implémentée » (...) le cas échéant. Le retour d’une erreur à partir d’un gestionnaire Table.View indique au moteur M que l’opération ne peut pas être repliée et qu’il doit plutôt revenir au gestionnaire par défaut (qui, dans ce cas, compte le nombre total de lignes).

Commencez par ajouter un test unitaire :

// GetRowCount
Fact("Fold $count", 15, Table.RowCount(Airlines)),

Étant donné que le segment de tracé /$count retourne une valeur unique (au format brut/texte) plutôt qu’un jeu de résultats JSON, vous devez également ajouter une nouvelle fonction interne (TripPin.Scalar) pour effectuer la requête et gérer le résultat.

// Similar to TripPin.Feed, but is expecting back a scalar value.
// This function returns the value from the service as plain text.
TripPin.Scalar = (url as text) as text =>
    let
        _url = Diagnostics.LogValue("TripPin.Scalar url", url),

        headers = DefaultRequestHeaders & [
            #"Accept" = "text/plain"
        ],

        response = Web.Contents(_url, [ Headers = headers ]),
        toText = Text.FromBinary(response)
    in
        toText;

L’implémentation utilise ensuite cette fonction (si aucun autre paramètre de requête n’est trouvé dans state) :

GetRowCount = () as number =>
    if (Record.FieldCount(Record.RemoveFields(state, {"Url", "Entity", "Schema"}, MissingField.Ignore)) > 0) then
        ...
    else
        let
            newState = state & [ RowCountOnly = true ],
            finalUrl = CalculateUrl(newState),
            value = TripPin.Scalar(finalUrl),
            converted = Number.FromText(value)
        in
            converted,

La fonction CalculateUrl est mise à jour pour ajouter /$count à l’URL si le champ RowCountOnly est défini dans state.

// Check for $count. If all we want is a row count,
// then we add /$count to the path value (following the entity name).
urlWithRowCount =
    if (state[RowCountOnly]? = true) then
        urlWithEntity & "/$count"
    else
        urlWithEntity,

Le nouveau test unitaire Table.RowCount doit maintenant réussir.

Pour tester le cas de secours, vous allez ajouter un autre test qui force l’erreur.

Commencez par ajouter une méthode d’assistance qui vérifie le résultat d’une opération try pour une erreur de pliage.

// Returns true if there is a folding error, or the original record (for logging purposes) if not.
Test.IsFoldingError = (tryResult as record) =>
    if ( tryResult[HasError]? = true and tryResult[Error][Message] = "We couldn't fold the expression to the data source. Please try a simpler expression.") then
        true
    else
        tryResult;

Ajoutez ensuite un test qui utilise Table.RowCount et Table.FirstN pour forcer l’erreur.

// test will fail if "Fail on Folding Error" is set to false
Fact("Fold $count + $top *error*", true, Test.IsFoldingError(try Table.RowCount(Table.FirstN(Airlines, 3)))),

Remarque importante : ce test retourne désormais une erreur si Erreur en cas d’échec de pliage est défini sur false, car l’opération Table.RowCount revient au gestionnaire local (par défaut). L’exécution des tests avec Erreur en cas d’échec de pliage défini sur true entraîne l’échec de Table.RowCount et permet au test de réussir.

Conclusion

L’implémentation de Table.View pour votre connecteur accroît de manière significative la complexité de votre code. Étant donné que le moteur M peut traiter toutes les transformations localement, l’ajout de gestionnaires Table.View ne permet pas d’entrevoir de nouveaux scénarios pour vos utilisateurs, mais améliore l’efficacité du traitement (et éventuellement la satisfaction des utilisateurs). L’un des principaux avantages du caractère facultatif des gestionnaires Table.View est que vous pouvez ajouter de façon incrémentielle de nouvelles fonctionnalités sans impacter la compatibilité descendante pour votre connecteur.

Pour la plupart des connecteurs, un gestionnaire important (et basique) à implémenter est OnTake (qui se traduit par $top dans OData), car il limite le nombre de lignes retournées. L’expérience Power Query effectue toujours un OnTake de 1000 lignes lors de l’affichage d’aperçus dans le navigateur et l’éditeur de requêtes. Vos utilisateurs pourront constater des améliorations de performances significatives quand ils se servent d’ensembles de données plus volumineux.