我使用spark-shell
进行以下操作:
最近在spark-sql中加载了一个包含数组列的表。
下面是相同的DDL:
create table test_emp_arr{
dept_id string,
dept_nm string,
emp_details Array<string>
}
数据看起来像这样
+-------+-------+-------------------------------+
|dept_id|dept_nm| emp_details|
+-------+-------+-------------------------------+
| 10|Finance|[Jon, Snow, Castle, Black, Ned]|
| 20| IT| [Ned, is, no, more]|
+-------+-------+-------------------------------+
我可以像这样查询emp_details列:
sqlContext.sql("select emp_details[0] from emp_details").show
我想查询集合中的元素范围:
期望查询工作
sqlContext.sql("select emp_details[0-2] from emp_details").show
或
sqlContext.sql("select emp_details[0:2] from emp_details").show
预期输出
+-------------------+
| emp_details|
+-------------------+
|[Jon, Snow, Castle]|
| [Ned, is, no]|
+-------------------+
在纯Scala中,如果我有一个数组:
val emp_details = Array("Jon","Snow","Castle","Black")
我可以使用
获得从0到2的元素范围emp_details.slice(0,3)
返回me
Array(Jon, Snow,Castle)
我无法在spark-sql中应用上述数组操作。
谢谢
从Spark 2.4开始可以使用slice
函数。Python在):
pyspark.sql.functions.slice(x, start, length)
集合函数:返回一个数组,其中包含x中从索引start开始(如果start为负数,则从末尾开始)指定长度的所有元素。
…
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]|
+-------------------+
Scala在<<p> strong> def slice(x: Column, start: Int, length: Int): Column
返回一个数组,其中包含x中从索引start开始(如果start为负数,则从末尾开始)指定长度的所有元素。
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]|
+-------------------+
同样的事情当然也可以在SQL
中完成SELECT slice(emp_details, 1, 3) AS emp_details FROM df
重要:
请注意,与Seq.slice
不同,值从0开始索引,第二个参数是长度,而不是结束位置。
这是一个使用用户定义函数的解决方案,它具有适用于任何您想要的切片大小的优点。它只是围绕scala内置slice
方法构建一个UDF函数:
import sqlContext.implicits._
import org.apache.spark.sql.functions._
val slice = udf((array : Seq[String], from : Int, to : Int) => array.slice(from,to))
数据示例:
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
产生预期的输出
+--------------------+-------------------+
| emp_details| slice|
+--------------------+-------------------+
|[Jon, Snow, Castl...|[Jon, Snow, Castle]|
+--------------------+-------------------+
您也可以在sqlContext
中注册UDF并像这样使用它
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)")
使用此解决方案,您将不再需要lit
Edit2:谁想以牺牲可读性来避免udf;-)
如果您真的想一步完成它,您将不得不使用Scala创建一个lambda函数,返回Column
序列并将其包装在数组中。这有点复杂,但这是一个步骤:
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]|
+-------------------------------+-------------------+
_:*
有点神奇,将一个列表传递给一个所谓的可变函数(在本例中是array
,它构造sql数组)。但我建议不要使用这个解决方案。将lambda函数放入命名函数
def slice(from: Int, to: Int) = array((from until to).map(i => $"emp_details"(i)):_*))
表示代码的可读性。请注意,一般来说,坚持使用Column
表达式(不使用' udf ')具有更好的性能。
编辑:为了在sql语句中做到这一点(正如您在问题中所问的…),遵循相同的逻辑,您将使用scala逻辑生成sql查询(不是说它是最可读的)
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]|
+-------------------------------+-------------------+
请注意,您可以将until
替换为to
,以便提供获取的最后一个元素,而不是迭代停止的元素。
您可以使用array
函数从三个值中构建一个新的Array:
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]|
// +-------------------+
在apache spark中使用selecrExpr()和split()函数
例如:
fs.selectExpr("((split(emp_details, ','))[0]) as e1,((split(emp_details, ','))[1]) as e2,((split(emp_details, ','))[2]) as e3);
这是我的通用切片UDF,支持任何类型的数组。这有点难看,因为你需要提前知道元素的类型。
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))
对于那些一直在使用Spark的人;2.4并且没有slice
函数,这里是pySpark (Scala将非常相似)中不使用udfs的解决方案。相反,它使用spark sql函数concat_ws
, substring_index
和split
。
这只适用于字符串数组。要使它与其他类型的数组一起工作,您必须首先将它们强制转换为字符串,然后在对数组进行"切片"后将其强制转换回原始类型。
from pyspark.sql import SparkSession
from pyspark.sql import functions as F
spark = (SparkSession.builder
.master('yarn')
.appName("array_slice")
.getOrCreate()
)
emp_details = [
["Jon", "Snow", "Castle", "Black", "Ned"],
["Ned", "is", "no", "more"]
]
df1 = spark.createDataFrame(
[tuple([emp]) for emp in emp_details],
["emp_details"]
)
df1.show(truncate=False)
+-------------------------------+
|emp_details |
+-------------------------------+
|[Jon, Snow, Castle, Black, Ned]|
|[Ned, is, no, more] |
+-------------------------------+
last_string = 2
df2 = (
df1
.withColumn('last_string', (F.lit(last_string)))
.withColumn('concat', F.concat_ws(" ", F.col('emp_details')))
.withColumn('slice', F.expr("substring_index(concat, ' ', last_string + 1)" ))
.withColumn('slice', F.split(F.col('slice'), ' '))
.select('emp_details', 'slice')
)
df2.show(truncate=False)
+-------------------------------+-------------------+
|emp_details |slice |
+-------------------------------+-------------------+
|[Jon, Snow, Castle, Black, Ned]|[Jon, Snow, Castle]|
|[Ned, is, no, more] |[Ned, is, no] |
+-------------------------------+-------------------+
使用嵌套分割:
split(split(concat_ws(',',emp_details),concat(',',emp_details[3]))[0],',')
scala> import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.SparkSession
scala> val spark=SparkSession.builder().getOrCreate()
spark: org.apache.spark.sql.SparkSession = org.apache.spark.sql.SparkSession@1d637673
scala> val df = spark.read.json("file:///Users/gengmei/Desktop/test/test.json")
18/12/11 10:09:32 WARN ObjectStore: Failed to get database global_temp, returning NoSuchObjectException
df: org.apache.spark.sql.DataFrame = [dept_id: bigint, dept_nm: string ... 1 more field]
scala> df.createOrReplaceTempView("raw_data")
scala> df.show()
+-------+-------+--------------------+
|dept_id|dept_nm| emp_details|
+-------+-------+--------------------+
| 10|Finance|[Jon, Snow, Castl...|
| 20| IT| [Ned, is, no, more]|
+-------+-------+--------------------+
scala> val df2 = spark.sql(
| s"""
| |select dept_id,dept_nm,split(split(concat_ws(',',emp_details),concat(',',emp_details[3]))[0],',') as emp_details from raw_data
| """)
df2: org.apache.spark.sql.DataFrame = [dept_id: bigint, dept_nm: string ... 1 more field]
scala> df2.show()
+-------+-------+-------------------+
|dept_id|dept_nm| emp_details|
+-------+-------+-------------------+
| 10|Finance|[Jon, Snow, Castle]|
| 20| IT| [Ned, is, no]|
+-------+-------+-------------------+