web-dev-qa-db-fra.com

sélection d'une plage d'éléments dans un tableau spark sql

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

6
thinkinbee

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

7
cheseaux

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.

2
Wilmerton

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]|
+-------------------+

En Scala

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.

1
user6910411

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]|
// +-------------------+
1
Tzach Zohar

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);
0
Kamal Pradhan

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))
0
Bewang