Steuerung des globalen Zustands in skalaren Scala-UDFs

Wenn Sie eine UDF und einen Handler entwerfen, die Zugriff auf einen gemeinsamen Zustand benötigen, müssen Sie berücksichtigen, wie Snowflake UDFs ausführt, um Zeilen zu verarbeiten.

Für die meisten Handler sollten folgende Richtlinien befolgt werden:

  • Wenn Sie einen gemeinsamen Zustand (Shared State) initialisieren müssen, der sich über Zeilen hinweg nicht ändert, initialisieren Sie ihn außerhalb der Handler-Funktion, z. B. in einem Konstruktor.

  • Schreiben Sie Ihre Handler-Methode so, dass sie Thread-sicher ist.

  • Vermeiden Sie das Speichern und Freigeben eines dynamischen Zustands über Zeilen hinweg.

Wenn Ihre UDF diesen Richtlinien nicht folgen kann oder wenn Sie ein tieferes Verständnis der Gründe für diese Richtlinien wünschen, lesen Sie die nächsten Unterabschnitte.

Gemeinsamer Zustand für alle Aufrufe (Shared State)

Snowflake erwartet, dass skalare UDFs unabhängig verarbeitet werden. Wenn Sie sich zwischen Aufrufen auf einen gemeinsamen Zustand verlassen, kann dies zu unerwartetem Verhalten führen. Dies liegt daran, dass das System Zeilen in beliebiger Reihenfolge verarbeiten kann und die Aufrufe auf mehrere JVMs (für in Java oder Scala geschriebene Handler) verteilt werden.

Bei UDFs sollte es vermieden werden, dass sich beim Aufrufen der Handler-Methode auf einen gemeinsamen Zustand verlassen wird. Es gibt jedoch zwei Situationen, in denen Sie möglicherweise möchten, dass eine UDF einen gemeinsamen Zustand speichert:

  • Code, der teure Initialisierungslogik enthält, die nicht bei jeder Zeile wiederholt werden soll.

  • Code, der einen gemeinsamen Zustand über Zeilen hinweg nutzt, z. B. als Cache.

Wenn Sie den Zustand über mehrere Zeilen hinweg gemeinsam nutzen müssen und sich dieser Zustand im Laufe der Zeit nicht ändert, dann verwenden Sie einen Konstruktor, um den gemeinsamen Zustand durch das Einstellen von Variablen auf Instanzenebene zu erstellen. Der Konstruktor wird nur einmal pro Instanz ausgeführt, während der Handler einmal pro Zeile aufgerufen wird, sodass die Initialisierung im Konstruktor günstiger ist, wenn ein Handler mehrere Zeilen verarbeitet. Und da der Konstruktor nur einmal aufgerufen wird, muss er nicht thread-sicher geschrieben werden.

Wenn Ihre UDF einen gemeinsamen Zustand speichert, der sich ändert, muss Ihr Code darauf vorbereitet sein, den gleichzeitigen Zugriff mit diesem Zustand zu verarbeiten.

Weitere Informationen zur Parallelität und zum gemeinsamen Zustand finden Sie unter Erläuterungen zur Parallelisierung und Speichern von JVM-Statusinformationen unter diesem Thema.

Erläuterungen zur Parallelisierung

Um die Verarbeitungsleistung zu verbessern, nutzt Snowflake die Parallelisierung sowohl zwischen mehrere JVMs als auch innerhalb von JVMs.

Parallelisierung zwischen mehreren JVMs

Snowflake parallelisiert mehrere Ausführungen in einem Warehouse. Jede Ausführung nutzt eine (oder mehrere) JVMs. Dies bedeutet, dass es keinen globalen gemeinsamen Zustand gibt. Der Zustand kann höchstens innerhalb einer einzelnen JVM gemeinsam genutzt werden.

Parallelisierung innerhalb von JVMs

  • Jede JVM kann mehrere Threads ausführen, die parallel die Handler-Methode der gleichen Instanz aufrufen können. Das bedeutet, dass jede Handler-Methode thread-sicher sein muss.

  • Wenn eine UDF mit IMMUTABLE eingerichtet ist und eine SQL-Anweisung diese UDF mehr als einmal mit denselben Argumenten für dieselbe Zeile aufruft, dann gibt die UDF bei jedem Aufruf für diese Zeile den gleichen Wert zurück.

    Im Folgenden wird z. B. zweimal derselbe Wert für jede Zeile zurückgegeben, wenn die UDF mit IMMUTABLE eingerichtet ist:

    SELECT my_scala_udf(42), my_scala_udf(42) FROM table1;
    
    Copy

    Wenn Sie möchten, dass mehrere Aufrufe unabhängige Werte zurückgeben, auch wenn die gleichen Argumente übergeben werden, und wenn Sie nicht die Funktion VOLATILE deklarieren möchten, dann binden Sie mehrere separate UDFs an die gleiche Handler-Methode.

    Dazu können Sie die folgenden Schritte ausführen.

    1. Erstellen Sie eine JAR-Datei mit dem Namen @udf_libs/rand.jar und mit dem folgenden Code:

      class MyClass {
      
          var x: Double = 0.0
      
          // Constructor
          def this() = {
              x = Math.random()
          }
      
          // Handler
          def myHandler(): Double = x
      }
      
      Copy
    2. Erstellen Sie die Scala-UDFs wie unten gezeigt.

      Diese UDFs haben unterschiedliche Namen, verwenden aber die gleiche JAR-Datei und innerhalb dieser JAR-Datei den gleichen Handler.

      CREATE FUNCTION my_scala_udf_1()
        RETURNS DOUBLE
        LANGUAGE SCALA
        IMPORTS = ('@udf_libs/rand.jar')
        HANDLER = 'MyClass.myHandler';
      
      CREATE FUNCTION my_scala_udf_2()
        RETURNS DOUBLE
        LANGUAGE SCALA
        IMPORTS = ('@udf_libs/rand.jar')
        HANDLER = 'MyClass.myHandler';
      
      Copy
    3. Verwenden Sie den folgenden Code, um beide UDFs aufzurufen.

      Die UDFs verweisen auf die gleiche JAR-Datei und den gleichen Handler. Diese Aufrufe erstellen zwei Instanzen der gleichen Klasse. Jede Instanz gibt einen unabhängigen Wert zurück, sodass das folgende Beispiel zwei unabhängige Werte zurückgibt, anstatt zweimal denselben Wert zu liefern:

      SELECT my_scala_udf_1(), my_scala_udf_2() FROM table1;
      
      Copy

Speichern von JVM-Statusinformationen

Ein Grund, sich nicht auf einen dynamischen gemeinsamen Zustand zu verlassen, besteht darin, dass Zeilen nicht unbedingt in einer vorhersehbaren Reihenfolge verarbeitet werden. Bei jeder Ausführung einer SQL-Anweisung kann Snowflake die Anzahl der Batches, die Reihenfolge, in der die Batches verarbeitet werden, und die Reihenfolge der Zeilen innerhalb eines Batches variieren. Wenn eine skalare UDF so gestaltet ist, dass eine Zeile den Rückgabewert einer nachfolgenden Zeile beeinflusst, dann kann die UDF bei jeder Ausführung unterschiedliche Ergebnisse zurückgeben.