UDT的Spark SQL引用属性

and*_*ove 5 user-defined-types apache-spark apache-spark-sql

我正在尝试实现自定义UDT并能够从Spark SQL引用它(如Spark SQL白皮书的第4.4.2节中所述)。

真正的例子是使用Cap'n Proto或类似方法,使自定义UDT由堆外数据结构提供支持。

对于这篇文章,我做了一个人为的例子。我知道我可以只使用Scala案例类,而不必做任何工作,但这不是我的目标。

例如,我有一个Person包含多个属性,并且希望能够SELECT person.first_name FROM person。我遇到了错误Can't extract value from person#1,但不确定为什么。

这是完整的源代码(也可以从https://github.com/andygrove/spark-sql-udt获取)。

package com.theotherandygrove

import org.apache.spark.sql.types._
import org.apache.spark.sql.{Row, SQLContext}
import org.apache.spark.{SparkConf, SparkContext}

object Example {

  def main(arg: Array[String]): Unit = {

    val conf = new SparkConf()
      .setAppName("Example")
      .setMaster("local[*]")

    val sc = new SparkContext(conf)

    val sqlContext = new SQLContext(sc)

    val schema = StructType(List(
      StructField("person_id", DataTypes.IntegerType, true),
      StructField("person", new MockPersonUDT, true)))

    // load initial RDD
    val rdd = sc.parallelize(List(
      MockPersonImpl(1),
      MockPersonImpl(2)
    ))

    // convert to RDD[Row]
    val rowRdd = rdd.map(person => Row(person.getAge, person))

    // convert to DataFrame (RDD + Schema)
    val dataFrame = sqlContext.createDataFrame(rowRdd, schema)

    // register as a table
    dataFrame.registerTempTable("person")

    // selecting the whole object works fine
    val results = sqlContext.sql("SELECT person.first_name FROM person WHERE person.age < 100")

    val people = results.collect

    people.map(row => {
      println(row)
    })

  }

}
Run Code Online (Sandbox Code Playgroud)
trait MockPerson {
  def getFirstName: String
  def getLastName: String
  def getAge: Integer
  def getState: String
}

class MockPersonUDT extends UserDefinedType[MockPerson] {

  override def sqlType: DataType = StructType(List(
    StructField("firstName", StringType, nullable=false),
    StructField("lastName", StringType, nullable=false),
    StructField("age", IntegerType, nullable=false),
    StructField("state", StringType, nullable=false)
  ))

  override def userClass: Class[MockPerson] = classOf[MockPerson]

  override def serialize(obj: Any): Any = obj.asInstanceOf[MockPersonImpl].getAge

  override def deserialize(datum: Any): MockPerson = MockPersonImpl(datum.asInstanceOf[Integer])
}

@SQLUserDefinedType(udt = classOf[MockPersonUDT])
@SerialVersionUID(123L)
case class MockPersonImpl(n: Integer) extends MockPerson with Serializable {
  def getFirstName = "First" + n
  def getLastName = "Last" + n
  def getAge = n
  def getState = "AK"
}
Run Code Online (Sandbox Code Playgroud)

如果我只是SELECT person FROM person那么查询工作。即使在架构中定义了属性,我也无法引用SQL中的属性。

zer*_*323 4

您会收到此错误,因为由 定义的架构sqlType永远不会公开,并且不适合直接访问。它只是提供了一种使用本机 Spark SQL 类型来表达复杂数据类型的方法。

您可以使用 UDF 访问各个属性,但首先让我们看看内部结构确实没有公开:

dataFrame.printSchema
// root
//  |-- person_id: integer (nullable = true)
//  |-- person: mockperso (nullable = true)
Run Code Online (Sandbox Code Playgroud)

要创建 UDF,我们需要将给定 UDT 表示的类型的对象作为参数的函数:

import org.apache.spark.sql.functions.udf

val getFirstName = (person: MockPerson) => person.getFirstName
val getLastName = (person: MockPerson) => person.getLastName
val getAge = (person: MockPerson) => person.getAge
Run Code Online (Sandbox Code Playgroud)

可以使用udf函数包装:

val getFirstNameUDF = udf(getFirstName)
val getLastNameUDF = udf(getLastName)
val getAgeUDF = udf(getAge)

dataFrame.select(
  getFirstNameUDF($"person").alias("first_name"),
  getLastNameUDF($"person").alias("last_name"),
  getAgeUDF($"person").alias("age")
).show()

// +----------+---------+---+
// |first_name|last_name|age|
// +----------+---------+---+
// |    First1|    Last1|  1|
// |    First2|    Last2|  2|
// +----------+---------+---+
Run Code Online (Sandbox Code Playgroud)

要将这些与原始 SQL 一起使用,您可以通过以下方式注册函数SQLContext

sqlContext.udf.register("first_name", getFirstName)
sqlContext.udf.register("last_name", getLastName)
sqlContext.udf.register("age", getAge)

sqlContext.sql("""
  SELECT first_name(person) AS first_name, last_name(person) AS last_name
  FROM person
  WHERE age(person) < 100""").show

// +----------+---------+
// |first_name|last_name|
// +----------+---------+
// |    First1|    Last1|
// |    First2|    Last2|
// +----------+---------+
Run Code Online (Sandbox Code Playgroud)

不幸的是它带有价格标签。首先,每个操作都需要反序列化。它还极大地限制了查询优化的方式。特别是join对这些字段之一的任何操作都需要笛卡尔积。

在实践中,如果你想编码一个复杂的结构,其中包含可以使用内置类型表达的属性,最好使用StructType

case class Person(first_name: String, last_name: String, age: Int)

val df = sc.parallelize(
  (1 to 2).map(i => (i, Person(s"First$i", s"Last$i", i)))).toDF("id", "person")

df.printSchema

// root
//  |-- id: integer (nullable = false)
//  |-- person: struct (nullable = true)
//  |    |-- first_name: string (nullable = true)
//  |    |-- last_name: string (nullable = true)
//  |    |-- age: integer (nullable = false)

df
  .where($"person.age" < 100)
  .select($"person.first_name", $"person.last_name")
  .show

// +----------+---------+
// |first_name|last_name|
// +----------+---------+
// |    First1|    Last1|
// |    First2|    Last2|
// +----------+---------+
Run Code Online (Sandbox Code Playgroud)

并为实际类型扩展(例如内置)或可以从特定表示(例如枚举)VectorUDT中受益的事物保留 UDT 。