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));
}
...
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));
}
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
}
}
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)
...
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 derendPartition
-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(...)); }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; }
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));
}
}
$$;
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() |
+-------------------------------------------------------+
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');
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() |
+-----------+-------------------------------------------------------+
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))
Das Verhalten würde dann folgendem Pseudocode entsprechen:
for city_name in cities_of_interest: output_row = f(city_name)
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 |
+-----------+-------------------------------------------------------+
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() |
+-----------+-------------------------------------------------------+
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.