J'utilise spark-Shell
pour effectuer les opérations ci-dessous.
Récemment chargé une table avec une colonne de tableau dans spark-sql.
Voici le DDL pour le même:
create table test_emp_arr{
dept_id string,
dept_nm string,
emp_details Array<string>
}
les données ressemblent à ceci
+-------+-------+-------------------------------+
|dept_id|dept_nm| emp_details|
+-------+-------+-------------------------------+
| 10|Finance|[Jon, Snow, Castle, Black, Ned]|
| 20| IT| [Ned, is, no, more]|
+-------+-------+-------------------------------+
Je peux interroger la colonne emp_details à peu près comme ceci:
sqlContext.sql("select emp_details[0] from emp_details").show
Problème
Je veux interroger une série d'éléments de la collection:
Requête attendue pour fonctionner
sqlContext.sql("select emp_details[0-2] from emp_details").show
ou
sqlContext.sql("select emp_details[0:2] from emp_details").show
Production attendue
+-------------------+
| emp_details|
+-------------------+
|[Jon, Snow, Castle]|
| [Ned, is, no]|
+-------------------+
En pure Scala, si j'ai un tableau, quelque chose comme:
val emp_details = Array("Jon","Snow","Castle","Black")
Je peux obtenir les éléments de 0 à 2 en utilisant
emp_details.slice(0,3)
me retourne
Array(Jon, Snow,Castle)
Je ne suis pas en mesure d'appliquer l'opération ci-dessus du tableau dans spark-sql.
Merci
Voici une solution utilisant une Fonction définie par l'utilisateur qui présente l'avantage de fonctionner pour toute taille de tranche que vous souhaitez. Il construit simplement une fonction UDF autour de la méthode scala intégrée slice
:
import sqlContext.implicits._
import org.Apache.spark.sql.functions._
val slice = udf((array : Seq[String], from : Int, to : Int) => array.slice(from,to))
Exemple avec un échantillon de vos données:
val df = sqlContext.sql("select array('Jon', 'Snow', 'Castle', 'Black', 'Ned') as emp_details")
df.withColumn("slice", slice($"emp_details", lit(0), lit(3))).show
Produit la sortie attendue
+--------------------+-------------------+
| emp_details| slice|
+--------------------+-------------------+
|[Jon, Snow, Castl...|[Jon, Snow, Castle]|
+--------------------+-------------------+
Vous pouvez également enregistrer le fichier UDF dans votre sqlContext
et l'utiliser comme ceci
sqlContext.udf.register("slice", (array : Seq[String], from : Int, to : Int) => array.slice(from,to))
sqlContext.sql("select array('Jon','Snow','Castle','Black','Ned'),slice(array('Jon','Snow','Castle','Black','Ned'),0,3)")
Vous n’avez plus besoin de lit
avec cette solution
Edit2: Pour qui veut éviter le format UDF au détriment de la lisibilité ;-)
Si vous voulez vraiment le faire en une étape, vous devrez utiliser Scala pour créer une fonction lambda renvoyant une séquence de Column
et l'envelopper dans un tableau. C'est un peu compliqué, mais c'est une étape:
val df = List(List("Jon", "Snow", "Castle", "Black", "Ned")).toDF("emp_details")
df.withColumn("slice", array((0 until 3).map(i => $"emp_details"(i)):_*)).show(false)
+-------------------------------+-------------------+
|emp_details |slice |
+-------------------------------+-------------------+
|[Jon, Snow, Castle, Black, Ned]|[Jon, Snow, Castle]|
+-------------------------------+-------------------+
Le _:*
fonctionne avec un peu de magie pour passer une liste à une fonction dite variadique (array
dans ce cas, qui construit le tableau SQL). Mais je vous conseillerais de ne pas utiliser cette solution telle quelle. mettre la fonction lambda dans une fonction nommée
def slice(from: Int, to: Int) = array((from until to).map(i => $"emp_details"(i)):_*))
pour la lisibilité du code. Notez qu'en général, s'en tenir aux expressions Column
(sans utiliser `udf) a de meilleures performances.
Edit: Afin de le faire dans une instruction SQL (comme vous le demandez dans votre question ...), en suivant la même logique, vous généreriez la requête SQL en utilisant la logique scala (sans dire que ce soit la plus lisible)
def sliceSql(emp_details: String, from: Int, to: Int): String = "Array(" + (from until to).map(i => "emp_details["+i.toString+"]").mkString(",") + ")"
val sqlQuery = "select emp_details,"+ sliceSql("emp_details",0,3) + "as slice from emp_details"
sqlContext.sql(sqlQuery).show
+-------------------------------+-------------------+
|emp_details |slice |
+-------------------------------+-------------------+
|[Jon, Snow, Castle, Black, Ned]|[Jon, Snow, Castle]|
+-------------------------------+-------------------+
notez que vous pouvez remplacer until
par to
afin de fournir le dernier élément pris plutôt que l'élément auquel l'itération s'arrête.
Depuis Spark 2.4, vous pouvez utiliser la fonction slice
. En Python ):
pyspark.sql.functions.slice(x, start, length)
Fonction de collection: renvoie un tableau contenant tous les éléments de x depuis le début de l'index (ou à partir de la fin si début est négatif) avec la longueur spécifiée.
...
Nouveau dans la version 2.4.
from pyspark.sql.functions import slice
df = spark.createDataFrame([
(10, "Finance", ["Jon", "Snow", "Castle", "Black", "Ned"]),
(20, "IT", ["Ned", "is", "no", "more"])
], ("dept_id", "dept_nm", "emp_details"))
df.select(slice("emp_details", 1, 3).alias("empt_details")).show()
+-------------------+
| empt_details|
+-------------------+
|[Jon, Snow, Castle]|
| [Ned, is, no]|
+-------------------+
def slice(x: Column, start: Int, length: Int): Column
Retourne un tableau contenant tous les éléments de x à partir du début de l'index (ou commençant à la fin si début est négatif) avec la longueur spécifiée.
import org.Apache.spark.sql.functions.slice
val df = Seq(
(10, "Finance", Seq("Jon", "Snow", "Castle", "Black", "Ned")),
(20, "IT", Seq("Ned", "is", "no", "more"))
).toDF("dept_id", "dept_nm", "emp_details")
df.select(slice($"emp_details", 1, 3) as "empt_details").show
+-------------------+
| empt_details|
+-------------------+
|[Jon, Snow, Castle]|
| [Ned, is, no]|
+-------------------+
La même chose peut être faite bien sûr dans SQL
SELECT slice(emp_details, 1, 3) AS emp_details FROM df
Important:
Veuillez noter que, contrairement à Seq.slice
, les valeurs sont indexées à partir de zéro et que le deuxième argument est longueur, pas la position finale.
Vous pouvez utiliser la fonction array
pour construire un nouveau tableau à partir des trois valeurs suivantes:
import org.Apache.spark.sql.functions._
val input = sqlContext.sql("select emp_details from emp_details")
val arr: Column = col("emp_details")
val result = input.select(array(arr(0), arr(1), arr(2)) as "emp_details")
val result.show()
// +-------------------+
// | emp_details|
// +-------------------+
// |[Jon, Snow, Castle]|
// | [Ned, is, no]|
// +-------------------+
utilisez selecrExpr () et split () function dans Apache spark.
par exemple :
fs.selectExpr("((split(emp_details, ','))[0]) as e1,((split(emp_details, ','))[1]) as e2,((split(emp_details, ','))[2]) as e3);
Voici mon UDF générique de tranche, supporte un tableau avec n'importe quel type. Un peu moche parce qu'il faut connaître le type d'élément à l'avance.
import org.Apache.spark.sql.types._
import org.Apache.spark.sql.functions._
def arraySlice(arr: Seq[AnyRef], from: Int, until: Int): Seq[AnyRef] =
if (arr == null) null else arr.slice(from, until)
def slice(elemType: DataType): UserDefinedFunction =
udf(arraySlice _, ArrayType(elemType)
fs.select(slice(StringType)($"emp_details", 1, 2))