Tabellarische Java-UDFs (UDTFs)

In diesem Dokument wird erläutert, wie eine UDTF (benutzerdefinierte Tabellenfunktion) in Java geschrieben wird.

Unter diesem Thema:

Einführung

Die Java-UDTF-Handler-Klasse verarbeitet die mit dem UDTF-Aufruf empfangenen Zeilen und gibt ein tabellarisches Ergebnis zurück. Die empfangenen Zeilen werden partitioniert, entweder implizit von Snowflake oder explizit in der Syntax des Funktionsaufrufs. Mit den Methoden, die Sie in der Klasse implementieren, können Sie sowohl einzelne Zeilen als auch die Partitionen, in die sie gruppiert sind, verarbeiten.

Zur Verarbeitung der Partitionen und Zeilen kann die Handler-Klasse Folgendes verwenden:

  • Argumentfreier Konstruktor als Initialisierer. Damit können Sie einen partitionsbezogenen Zustand einrichten.

  • Methode process zur Verarbeitung jeder Zeile.

  • Argumentfreie Methode endPartition als Finalizer, um die Partitionsverarbeitung abzuschließen, einschließlich der Rückgabe eines Wertes, der nur für die Partition gültig ist.

Weitere Informationen dazu finden Sie unter Java-Klassen für UDTFs (unter diesem Thema).

Jede Java-UDTF erfordert auch eine Ausgabezeilenklasse, die die Java-Datentypen der Spalten der Ausgabezeile(n) angibt, die von der Handler-Klasse generiert werden. Weitere Informationen dazu finden Sie unter Die Ausgabezeilenklasse (unter diesem Thema).

Nutzungshinweise für die Partitionierung

  • Wenn Zeilen empfangen werden, die von Snowflake implizit partitioniert sind, kann Ihr Handler-Code keine Annahmen über Partitionen treffen. Eine Ausführung mit impliziter Partitionierung ist vor allem dann sinnvoll, wenn die UDTF zum Erzeugen der Ausgabe Zeilen nur isoliert betrachten muss und ein Zustand nicht über Zeilen hinweg aggregiert wird. In diesem Fall braucht der Code wahrscheinlich weder einen Konstruktor noch eine endPartition-Methode.

  • Zur Verbesserung der Verarbeitungsleistung führt Snowflake normalerweise mehrere Instanzen des Handler-Codes der UDTF parallel aus. Jede Partition von Zeilen wird an eine einzelne Instanz der UDTF übergeben.

  • Obwohl jede Partition nur von einer einzigen UDTF-Instanz verarbeitet wird, gilt die Umkehrung nicht: Eine UDTF-Instanz kann mehrere Partitionen nacheinander verarbeiten. Daher ist es wichtig, den Initialisierer und den Finalizer zur Initialisierung und Bereinigung für jede Partition zu verwenden, um zu vermeiden, dass akkumulierte Werte von der Verarbeitung einer Partition zur Verarbeitung einer anderen Partition übertragen werden.

Bemerkung

Tabellarische Funktionen (UDTFs) haben einen Grenzwert von 500 Eingabeargumenten und 500 Ausgabespalten.

Java-Klassen für UDTFs

Die Hauptkomponenten von UDTF sind die Handler-Klasse und die Ausgabezeilenklasse.

Die Handler-Klasse

Snowflake interagiert mit der UDTF in erster Linie über Aufrufe der folgenden Methoden der Handler-Klasse:

  • Initialisierer (der Konstruktor)

  • Zeilenmethode (process)

  • Finalisierungsmethode (endPartition)

Die Handler-Klasse kann zusätzliche Methoden enthalten, die zur Unterstützung dieser drei Methoden erforderlich sind.

Die Handler-Klasse enthält auch eine Methode getOutputClass, die weiter unten beschrieben wird.

Das Auslösen einer Ausnahme durch eine beliebige Methode der Handler-Klasse (oder der Ausgabezeilenklasse) führt zum Abbruch der Verarbeitung. Die Abfrage, mit der die UDTF aufgerufen wurde, schlägt mit einer Fehlermeldung fehl.

Der Konstruktor

Eine Handler-Klasse kann einen Konstruktor haben, der keine Argumente annimmt.

Der Konstruktor wird einmal für jede Partition aufgerufen, bevor process aufgerufen wird.

Der Konstruktor kann keine Ausgabezeilen erzeugen.

Verwenden Sie den Konstruktor, um den Zustand der Partition zu initialisieren. Dieser Zustand kann von den Methoden process und endPartition verwendet werden. Der Konstruktor eignet sich auch gut bei einer langwierigen Initialisierung, die nur einmal pro Partition und nicht einmal pro Zeile erfolgen muss.

Der Konstruktor ist optional.

Die process-Methode

Die process-Funktion wird einmal für jede Zeile der Eingabepartition aufgerufen.

Die an die UDTF übergebenen Argumente werden an process übergeben. Die Werte der Argumente werden von SQL-Datentypen in Java-Datentypen konvertiert. (Weitere Informationen zur Zuordnung von SQL- zu Java-Datentypen finden Sie unter Zuordnung von Datentypen zwischen SQL und Java.)

Die Parameternamen der process-Methode können beliebige gültige Java-Bezeichner sein. Die Namen müssen nicht mit den in der CREATE FUNCTION-Anweisung angegebenen Namen übereinstimmen.

Bei jedem Aufruf von process können null, eine oder mehrere Zeilen zurückgeben werden.

Der von der process-Methode zurückgegebene Datentyp muss Stream<OutputRow> sein, wobei „Stream“ in java.util.stream.Stream definiert ist und OutputRow der Klassenname der Ausgabezeile ist. Im folgenden Beispiel wird eine einfache process-Methode verwendet, die lediglich die Eingabe über einen Stream zurückgibt:

import java.util.stream.Stream;

...

public Stream<OutputRow> process(String v) {
  return Stream.of(new OutputRow(v));
}

...
Copy

Wenn die Methode process keinen Zustand des Objekts beibehält oder verwendet (z. B. wenn die Methode nur ausgewählte Eingabezeilen von der Ausgabe ausschließen soll), können Sie die Methode static deklarieren. Wenn die process-Methode static ist und die Handler-Klasse keinen Konstruktor oder eine nicht statische endPartition-Methode hat, übergibt Snowflake jede Zeile direkt an die statische process-Methode, ohne eine Instanz der Handler-Klasse zu erzeugen.

Wenn Sie eine Eingabezeile überspringen und die nächste Zeile verarbeiten müssen (z. B. wenn Sie die Eingabezeilen validieren), geben Sie ein leeres Stream-Objekt zurück. Die nachstehende process-Methode gibt zum Beispiel nur die Zeilen zurück, für die number eine positive ganze Zahl ist. Wenn number nicht positiv ist, gibt die Methode ein leeres Stream-Objekt zurück, um die aktuelle Zeile zu überspringen und mit der Verarbeitung der nächsten Zeile fortzufahren.

public Stream<OutputRow> process(int number) {
  if (inputNumber < 1) {
    return Stream.empty();
  }
  return Stream.of(new OutputRow(number));
}
Copy

Wenn process einen Null-Stream zurückgibt, wird die Verarbeitung abgebrochen. (Die endPartition-Methode wird auch dann aufgerufen, wenn ein Null-Stream zurückgegeben wird.)

Diese Methode ist veraltet.

Die endPartition-Methode

Diese optionale Methode kann verwendet werden, um Ausgabezeilen zu generieren, die auf beliebigen, in process aggregierten Zustandsinformationen basieren. Diese Methode wird einmal für jede Partition aufgerufen, nachdem alle Zeilen in dieser Partition an process übergeben wurden.

Wenn Sie diese Methode einschließen, wird sie für jede Partition aufgerufen, unabhängig davon, ob die Daten explizit oder implizit partitioniert wurden. Wenn die Daten nicht sinnvoll partitioniert sind, ist die Ausgabe des Finalizers möglicherweise nicht sinnvoll.

Bemerkung

Wenn der Benutzer die Daten nicht explizit partitioniert, dann partitioniert Snowflake die Daten implizit. Weitere Informationen dazu finden Sie unter Partitionen.

Diese Methode kann null, eine oder mehrere Zeilen ausgeben.

Bemerkung

Snowflake unterstützt zwar große Partitionen mit Timeouts, die so eingestellt sind, dass sie erfolgreich verarbeitet werden können, aber bei besonders großen Partitionen kann es zu Zeitüberschreitungen kommen (z. B. wenn endPartition zu lange für den Abschluss braucht). Wenden Sie sich an den Snowflake-Support, wenn Sie den Timeout-Schwellenwert für bestimmte Nutzungsszenarios anpassen möchten.

Die getOutputClass-Methode

Diese Methode gibt Informationen über die Ausgabezeilenklasse zurück. Die Ausgabezeilenklasse enthält Informationen zu den Datentypen der zurückgegebenen Zeile.

Die Ausgabezeilenklasse

Snowflake verwendet die Ausgabezeilenklasse, um Konvertierungen zwischen Java-Datentypen und SQL-Datentypen zu spezifizieren.

Wenn eine Java-UDTF eine Zeile zurückgibt, muss der Wert jeder Spalte der Zeile vom Java-Datentyp in den entsprechenden SQL-Datentyp konvertiert werden. Die SQL-Datentypen werden in der RETURNS-Klausel der CREATE FUNCTION-Anweisung angegeben. Die Zuordnung von Java- zu SQL-Datentypen ist jedoch nicht 1:1, sodass Snowflake den Java-Datentyp für jede zurückgegebene Spalte kennen muss. (Weitere Informationen zur Zuordnung von SQL- zu Java-Datentypen finden Sie unter Zuordnung von Datentypen zwischen SQL und Java.)

Eine Java-UDTF spezifiziert die Java-Datentypen der Ausgabespalten durch die Definition einer Ausgabezeilenklasse. Jede von der UDTF zurückgegebene Zeile wird als Instanz der Ausgabezeilenklasse zurückgegeben. Jede Instanz der Ausgabezeilenklasse enthält ein öffentliches Feld für jede Ausgabespalte. Snowflake liest die Werte der öffentlichen Felder aus jeder Instanz der Ausgabezeilenklasse, konvertiert die Java-Werte in SQL-Werte und konstruiert eine SQL-Ausgabezeile, die diese Werte enthält.

Die Werte in jeder Instanz der Ausgabezeilenklasse werden durch den Aufruf des Konstruktors der Ausgabezeilenklasse festgelegt. Der Konstruktor nimmt Parameter entgegen, die den Ausgabespalten entsprechen, und setzt dann die öffentlichen Felder auf diese Parameter.

Der folgende Code definiert ein Beispiel für eine Ausgabezeilenklasse:

class OutputRow {

  public String name;
  public int id;

  public OutputRow(String pName, int pId) {
    this.name = pName;
    this.id = pId
  }

}
Copy

Die von dieser Klasse angegebenen öffentlichen Variablen müssen mit den in der RETURNS TABLE (...)-Klausel der CREATE FUNCTION-Anweisung angegebenen Spalten übereinstimmen. Beispielsweise entspricht die obige OutputRow-Klasse der folgenden RETURNS-Klausel:

CREATE FUNCTION F(...)
    RETURNS TABLE(NAME VARCHAR, ID INTEGER)
    ...
Copy

Wichtig

Der Abgleich zwischen den SQL-Spaltennamen und den Namen der öffentlichen Java-Felder in der Ausgabezeilenklasse erfolgt ohne Berücksichtigung der Groß-/Kleinschreibung. Im oben gezeigten Java- und SQL-Code entspricht beispielsweise das Java-Feld mit dem Namen id der SQL-Spalte mit dem Namen ID.

Die Ausgabezeilenklasse wird wie folgt verwendet:

  • Die Handler-Klasse verwendet die Ausgabezeilenklasse, um den Rückgabetyp der process-Methode und der endPartition-Methode anzugeben. Die Handler-Klasse verwendet auch die Ausgabezeilenklasse, um die zurückgegebenen Werte zu konstruieren. Beispiel:

    public Stream<OutputRow> process(String v) {
      ...
      return Stream.of(new OutputRow(...));
    }
    
    public Stream<OutputRow> endPartition() {
      ...
      return Stream.of(new OutputRow(...));
    }
    
    Copy
  • Die Ausgabezeilenklasse wird auch in der getOutputClass-Methode der Handler-Klasse verwendet, die eine statische Methode ist, mit der Snowflake aufgerufen wird, um die Java-Datentypen der Ausgabe zu erfahren:

    public static Class getOutputClass() {
      return OutputRow.class;
    }
    
    Copy

Das Auslösen einer Ausnahme durch eine beliebige Methode der Ausgabezeilenklasse (oder der Handler-Klasse) führt zum Abbruch der Verarbeitung. Die Abfrage, mit der die UDTF aufgerufen wurde, schlägt mit einer Fehlermeldung fehl.

Zusammenfassung der Anforderungen

Der Java-Code der UDTFmuss die folgenden Anforderungen erfüllen:

  • Der Code muss eine Ausgabezeilenklasse definieren.

  • Die Handler-Klasse der UDTF muss eine öffentliche Methode namens process enthalten, die einen „Stream“ der <Ausgabezeilenklasse> zurückgibt, wobei „Stream“ in java.util.stream.Stream definiert ist.

  • Die Handler-Klasse der UDTF muss eine öffentliche statische Methode namens getOutputClass definieren, die <Ausgabezeilenklasse>.class zurückgeben muss.

Wenn der Java-Code diese Anforderungen nicht erfüllt, schlägt entweder die Erstellung oder die Ausführung der UDTF fehl:

  • Wenn die Sitzung zum Zeitpunkt der Ausführung der CREATE FUNCTION-Anweisung über ein aktives Warehouse verfügt, erkennt Snowflake mögliche Verstöße beim Erstellen der Funktion.

  • Wenn die Sitzung zum Zeitpunkt der Ausführung der CREATE FUNCTION-Anweisung kein aktives Warehouse hat, erkennt Snowflake mögliche Verstöße beim Aufrufen der Funktion.

Beispiele für den Aufruf von Java-UDTFs in Abfragen

Allgemeine Informationen zum Aufrufen von UDFs und UDTFs finden Sie unter Aufrufen einer UDF.

Aufruf ohne explizite Partitionierung

Im folgenden Beispiel wird das Erstellen einer UDTF gezeigt: In diesem Beispiel werden zwei Kopien jeder Eingabe zurückgegeben, und für jede Partition wird eine zusätzliche Zeile zurückgegeben.

create function return_two_copies(v varchar)
returns table(output_value varchar)
language java
handler='TestFunction'
target_path='@~/TestFunction.jar'
as
$$

  import java.util.stream.Stream;

  class OutputRow {

    public String output_value;

    public OutputRow(String outputValue) {
      this.output_value = outputValue;
    }

  }


  class TestFunction {

    String myString;

    public TestFunction()  {
      myString = "Created in constructor and output from endPartition()";
    }

    public static Class getOutputClass() {
      return OutputRow.class;
    }

    public Stream<OutputRow> process(String inputValue) {
      // Return two rows with the same value.
      return Stream.of(new OutputRow(inputValue), new OutputRow(inputValue));
    }

    public Stream<OutputRow> endPartition() {
      // Returns the value we initialized in the constructor.
      return Stream.of(new OutputRow(myString));
    }

  }

$$;
Copy

Dieses Beispiel zeigt, wie eine UDTF aufgerufen wird. Um dieses Beispiel einfach zu halten, übergibt die Anweisung keine Spalte sondern einen Literalwert und vermeidet die OVER()-Klausel.

SELECT output_value
   FROM TABLE(return_two_copies('Input string'));
+-------------------------------------------------------+
| OUTPUT_VALUE                                          |
|-------------------------------------------------------|
| Input string                                          |
| Input string                                          |
| Created in constructor and output from endPartition() |
+-------------------------------------------------------+
Copy

In diesem Beispiel wird die UDTF mit Werten aufgerufen, die aus einer anderen Tabelle gelesen wurden. Jedes Mal, wenn die process-Methode aufgerufen wird, wird ihr ein Wert aus der Spalte city_name der aktuellen Zeile der Tabelle cities_of_interest übergeben. Wie oben wird die UDTF ohne explizite OVER()-Klausel aufgerufen.

Erstellen Sie eine einfache Tabelle, die als Quelle für Eingaben verwendet werden kann:

CREATE TABLE cities_of_interest (city_name VARCHAR);
INSERT INTO cities_of_interest (city_name) VALUES
    ('Toronto'),
    ('Warsaw'),
    ('Kyoto');
Copy

Rufen Sie die Java-UDTF auf:

SELECT city_name, output_value
   FROM cities_of_interest,
       TABLE(return_two_copies(city_name))
   ORDER BY city_name, output_value;
+-----------+-------------------------------------------------------+
| CITY_NAME | OUTPUT_VALUE                                          |
|-----------+-------------------------------------------------------|
| Kyoto     | Kyoto                                                 |
| Kyoto     | Kyoto                                                 |
| Toronto   | Toronto                                               |
| Toronto   | Toronto                                               |
| Warsaw    | Warsaw                                                |
| Warsaw    | Warsaw                                                |
| NULL      | Created in constructor and output from endPartition() |
+-----------+-------------------------------------------------------+
Copy

Achtung

In diesem Beispiel ist die in der FROM-Klausel verwendete Syntax identisch mit der Syntax einer inneren Verknüpfung (d. h. FROM t1, t2). Die ausgeführte Operation ist jedoch keine echte innere Verknüpfung. Tatsächlich ist das Verhalten so, dass die Funktion mit den Werten aus jeder Zeile der Tabelle aufgerufen wird. Angenommen, die folgende FROM-Klausel ist wie folgt:

from cities_of_interest, table(f(city_name))
Copy

Das Verhalten würde dann folgendem Pseudocode entsprechen:

for city_name in cities_of_interest:
    output_row = f(city_name)
Copy

Der Beispielabschnitt in der Dokumentation zu JavaScript-UDTFs enthält komplexere Beispiele für Abfragen, die UDTFs mit Werten aus Tabellen aufrufen.

Wenn die Anweisung die Partitionierung nicht explizit angibt, verwendet das Snowflake-Ausführungsmodul die implizite Partitionierung.

Wenn es nur eine einzige Partition gibt, wird die endPartition-Methode nur einmal aufgerufen, und die Ausgabe der Abfrage enthält nur eine Zeile mit dem Wert Created in constructor and output from endPartition(). Wenn die Daten bei verschiedenen Ausführungen der Anweisung in eine unterschiedliche Anzahl von Partitionen gruppiert werden, wird die endPartition-Methode unterschiedlich oft aufgerufen, und die Ausgabe enthält eine unterschiedliche Anzahl von Kopien dieser Zeile.

Weitere Informationen dazu finden Sie unter implizite Partitionierung.

Aufruf mit expliziter Partitionierung

Java-UDTFs können auch durch explizite Partitionierung aufgerufen werden.

Mehrere Partitionen

Im folgenden Beispiel werden dieselbe UDTF und die zuvor erstellte Tabelle verwendet. Im Beispiel werden die Daten nach „city_name“ partitioniert.

SELECT city_name, output_value
   FROM cities_of_interest,
       TABLE(return_two_copies(city_name) OVER (PARTITION BY city_name))
   ORDER BY city_name, output_value;
+-----------+-------------------------------------------------------+
| CITY_NAME | OUTPUT_VALUE                                          |
|-----------+-------------------------------------------------------|
| Kyoto     | Created in constructor and output from endPartition() |
| Kyoto     | Kyoto                                                 |
| Kyoto     | Kyoto                                                 |
| Toronto   | Created in constructor and output from endPartition() |
| Toronto   | Toronto                                               |
| Toronto   | Toronto                                               |
| Warsaw    | Created in constructor and output from endPartition() |
| Warsaw    | Warsaw                                                |
| Warsaw    | Warsaw                                                |
+-----------+-------------------------------------------------------+
Copy

Einzelne Partition

Im folgenden Beispiel werden dieselbe UDTF und Tabelle verwendet, die zuvor erstellt wurden, und partitioniert die Daten durch eine Konstante, die Snowflake dazu zwingt, nur eine einzige Partition zu verwenden:

SELECT city_name, output_value
   FROM cities_of_interest,
       TABLE(return_two_copies(city_name) OVER (PARTITION BY 1))
   ORDER BY city_name, output_value;
+-----------+-------------------------------------------------------+
| CITY_NAME | OUTPUT_VALUE                                          |
|-----------+-------------------------------------------------------|
| Kyoto     | Kyoto                                                 |
| Kyoto     | Kyoto                                                 |
| Toronto   | Toronto                                               |
| Toronto   | Toronto                                               |
| Warsaw    | Warsaw                                                |
| Warsaw    | Warsaw                                                |
| NULL      | Created in constructor and output from endPartition() |
+-----------+-------------------------------------------------------+
Copy

Beachten Sie, dass nur eine Kopie der Meldung Created in constructor and output from endPartition() in der Ausgabe enthalten ist, was darauf hinweist, dass endPartition nur einmal aufgerufen wurde.

Verarbeiten sehr großer Eingaben (z. B. großer Dateien)

In einigen Fällen benötigt eine UDTF sehr viel Arbeitsspeicher, um jede Eingabezeile verarbeiten zu können. Eine UDTF könnte zum Beispiel eine Datei lesen und verarbeiten, die zu groß ist, um in den Arbeitsspeicher zu passen.

Um große Dateien in einer UDF oder UDTF zu verarbeiten, verwenden Sie die Klasse SnowflakeFile oder InputStream. Weitere Informationen dazu finden Sie unter Verarbeiten von unstrukturierten Daten mit UDF- und Prozedur-Handlern.