UDF Python, UDF Scala et les types de données complexes dans le moteur d’exécution natif

Le moteur d’exécution natif de Microsoft Fabric prend désormais en charge les fonctions définies par l’utilisateur (UDF) Python, les UDF Scala et les types de données complexes (tableaux, mappages et structs). Ces fonctionnalités vous permettent d’écrire des applications Spark expressives sans sacrifier les performances.

Prise en charge des UDF Python

Python est l’un des langages les plus populaires dans l’ingénierie des données et la science des données. Historiquement, les UDF Python ont entraîné une surcharge importante dans Spark en raison des coûts de sérialisation entre la JVM et les processus workers Python. Le moteur d’exécution natif réduit ces transitions coûteuses, ce qui accélère l’exécution sans modification du code.

Fonctionnement des fonctions Python définies par l’utilisateur dans le moteur d’exécution natif

Dans un modèle d’exécution Spark classique, l’exécution des UDF Python implique :

  1. Conversion de données à partir du format interne de Spark.
  2. Sérialisation et transfert vers les processus de travail Python.
  3. Python exécution UDF.
  4. Sérialisation des résultats dans la machine virtuelle JVM.
  5. Spark reprend l’exécution.

Ces transferts entre environnements d’exécution entraînent des coûts de sérialisation et de désérialisation, une utilisation inefficace du processeur et des pipelines d’exécution colonnaire défaillants. Le moteur d’exécution natif réduit cette surcharge en optimisant le chemin de transfert de données et en conservant le traitement vectorisé si possible.

Types d’UDF Python pris en charge

Le moteur d’exécution natif prend en charge :

  • Scalar UDFs : fonctions de Python ligne par ligne inscrites auprès de udf().
  • Fonctions définies par l’utilisateur (UDF) vectorisées (Pandas) : fonctions décorées avec @pandas_udf, qui traitent des lots de données à l’aide d’Apache Arrow pour un transfert efficace.

Les UDF vectorisées offrent les gains de performances les plus importants, car elles s’alignent naturellement sur le modèle de traitement colonnaire du moteur d’exécution natif.

Exemple : UDF vectorisée Python

import pandas as pd
from pyspark.sql.functions import pandas_udf
from pyspark.sql.types import DoubleType

@pandas_udf(DoubleType())
def calculate_discount(price: pd.Series, rate: pd.Series) -> pd.Series:
    return price * (1 - rate)

df = spark.table("sales.transactions")
result = df.withColumn("discounted_price", calculate_discount(df.price, df.discount_rate))
result.show()

Aucune configuration supplémentaire n’est requise au-delà de l’activation du moteur d’exécution natif. Les UDF Python existantes bénéficient automatiquement.

Prise en charge des UDF Scala

Le moteur d’exécution natif accélère également les fonctions UDF Scala. Comme les UDF Scala s’exécutent nativement sur la JVM, le moteur peut déléguer les opérations prises en charge au pipeline d’exécution C++ vectorisé tout en maintenant l’efficacité de l’évaluation des UDF Scala dans le même environnement d’exécution.

Exemple : UDF Scala

import org.apache.spark.sql.functions.udf

val toUpperCase = udf((s: String) => s.toUpperCase)
val df = spark.table("catalog.customers")
val result = df.withColumn("name_upper", toUpperCase(df("name")))
result.show()

Les UDF Scala qui utilisent des types de données pris en charge sont accélérées sans modifier le code lorsque le moteur d’exécution natif est activé.

Prise en charge des types de données complexes

Les architectures lakehouse modernes dépendent de données semi-structurées et imbriquées. Le moteur d’exécution natif fournit désormais une prise en charge optimisée pour :

Type de données Description Exemple de cas d’usage
Tableau Collection ordonnée d’éléments Étiquettes d’événements, catégories de produits
Carte Paires clé-valeur Propriétés de configuration, métadonnées
Struct Champs nommés de types différents Enregistrements clients imbriqués, objets d’adresse

Opérations prises en charge pour les types complexes

Le moteur d’exécution natif accélère les opérations courantes sur les types de données complexes :

  • Fonctions de tableau : explode, , array_containssize, flattentransform
  • Fonctions cartographiques : map_keys, map_values, element_at
  • Accès au struct : accès aux champs avec la notation par point, getField
  • Combinaisons imbriquées : tableaux de structures, associations avec des valeurs de type tableau

Exemple : Travailler avec des tableaux et des structures

from pyspark.sql.functions import explode, col, size

# Read data with nested schema
df = spark.table("events.telemetry")

# Operations on arrays - accelerated by native engine
result = (df
    .filter(size(col("tags")) > 0)
    .select(
        col("event_id"),
        col("metadata.source"),  # Struct field access
        explode(col("tags")).alias("tag")
    )
)
result.show()

Exemple : Utilisation de cartes

from pyspark.sql.functions import map_keys, map_values, col

df = spark.table("config.settings")

# Map operations - accelerated by native engine
result = (df
    .select(
        col("setting_id"),
        map_keys(col("properties")).alias("keys"),
        map_values(col("properties")).alias("values")
    )
)
result.show()

Résultats des performances

Les tests de performance internes montrent des améliorations significatives dans l’ensemble des charges de travail qui utilisent des UDF Python et des types de données complexes :

Type de charge de travail Amélioration des performances
Fonctions définies par l’utilisateur Python vectorisées Jusqu’à 5,76 fois plus rapidement
UDF Python scalaires Jusqu’à 1,08x plus rapide
TPC-DS de bout en bout (avec des types complexes) Jusqu’à 2,35 fois plus rapidement

Ces gains résultent de la réduction de la surcharge de sérialisation, de la vectorisation améliorée et de l’exécution en colonnes de bout en bout.

Avantages des schémas lakehouse avancés

L’accélération des types de données complexes est particulièrement importante pour :

  • Optimisation Z-ORDER : les colonnes imbriquées participent à une organisation optimisée des données.
  • Clustering liquide : les colonnes de type complexe tirent parti du clustering sans avoir à être aplaties.
  • Analytique semi-structurée : les charges utiles JSON et les flux d’événements restent imbriqués pour l’interrogation naturelle.
  • Architectures pilotées par les événements : les données de télémétrie et IoT conservent leur structure hiérarchique.

Au lieu d’aplatir les données ou de restructurer les pipelines pour améliorer les performances, travaillez naturellement avec des schémas de données complexes tout en maintenant une grande efficacité d’exécution.

Activer la fonctionnalité

La prise en charge des UDF Python, des UDF Scala et des types de données complexes est disponible lorsque le moteur d’exécution natif est activé. Aucune configuration supplémentaire n’est nécessaire.

Pour activer le moteur d’exécution natif, consultez le moteur d’exécution natif pour l’ingénierie des données Fabric.

Prerequisites

Limitations

  • Toutes les bibliothèques Python ne sont pas prises en charge dans le chemin vectorisé. Les bibliothèques qui nécessitent la sérialisation d’objets Python arbitraires peuvent encore activer un mécanisme de repli.
  • Les types complexes profondément imbriqués (par exemple, les tableaux de cartes de structs) peuvent revenir au moteur JVM pour certaines opérations.
  • Le mode ANSI n’est pas pris en charge avec le moteur d’exécution natif.