文章目录
前言
Spark 的数据源分为内部数据源和外部数据源,本文将讨论Spark SQL 与外部数据源之间的交互:
- Spark 和 Hive 都会使用到的 UDF 函数
- 通过 JDBC 连接各种外部数据源
- 简单和复杂的数据类型和各种高阶运算符
还将了解使用Spark SQL查询Spark的一些不同工具,例如Spark SQL shell、Beeline和Tableau。
Spark SQL 与 Hive
Spark SQL 让 Spark 使用者编写的查询语句拥有更好的性能和更简单的写法(例如,声明式查询和优化过的存储),以及调用复杂的分析库(例如,机器学习)。并且,SparkSession提供了一个单一的统一入口点来操作Spark中的数据。
UDF
创建自己的PySpark或Scala UDF(user defined function)的好处是我们将能够在Spark SQL中使用它们, 而不需要关系该方法的内部实现。例如,数据科学家可以将ML模型包装在UDF中,以便数据分析师可以在Spark SQL中查询与分析,而不必了解模型的内部结构。
Spark SQL UDFs
创建
scala
// In Scala
// Create cubed function
val cubed = (s: Long) => {
s * s * s
}
// Register UDF
spark.udf.register("cubed", cubed)
// Create temporary view
spark.range(1, 9).createOrReplaceTempView("udf_test")
python
# In Python
from pyspark.sql.types import LongType
# Create cubed function
def cubed(s):
return s * s * s
# Register UDF
spark.udf.register("cubed", cubed, LongType())
# Generate temporary view
spark.range(1, 9).createOrReplaceTempView("udf_test")
创建完后使用:
python
// In Scala/Python
// Query the cubed UDF
spark.sql("SELECT id, cubed(id) AS id_cubed FROM udf_test").show()
+---+--------+
| id|id_cubed|
+---+--------+
| 1| 1|
| 2| 8|
| 3| 27|
| 4| 64|
| 5| 125|
| 6| 216|
| 7| 343|
| 8| 512|
+---+--------+
PySpark 与 Pandas UDFs
在早期的时候通过 PySpark 使用 UDF 会使查询性能变慢,因为 python 的UDF 需要转化为 JVM 程序,为了解决这个问题,Spark 2.3 之后 Pandas UDF 被集成进了 Spark,只要数据是Apache Arrow格式,就不再需要序列化/反序列化数据。
Spark 3.0 之后,Pandas UDF 被拆分为Pandas UDFs 和 Pandas Function APIs.
-
Pandas UDFs
在Apache Spark 3.0中,Pandas UDFs(用户定义函数)可以从Pandas UDFs中的Python类型提示中推断出Pandas UDF类型,例如pandas.Series、pandas.DataFrame、Tuple和Iterator。以前,需要手动定义和指定每个Pandas UDF类型。当前,Pandas UDFs中支持的Python类型提示的情况包括Series到Series、Series的Iterator到Series的Iterator、多个Series的Iterator到Series的Iterator,以及Series到Scalar(单个值)
-
Pandas Function APIs
Pandas API允许直接将本地 Python 函数应用于 PySpark DataFrame,其中输入和输出都是Pandas实例。对于Spark 3.0,支持的Pandas函数API包括分组映射(grouped map)、映射(map)和联合分组映射(co-grouped map).
创建 UDF
python
# In Python
# Import pandas
import pandas as pd
# Import various pyspark SQL functions including pandas_udf
from pyspark.sql.functions import col, pandas_udf
from pyspark.sql.types import LongType
# Declare the cubed function
def cubed(a: pd.Series) -> pd.Series:
return a * a * a
# Create the pandas UDF for the cubed function
cubed_udf = pandas_udf(cubed, returnType=LongType())
通过 Pandas 使用该 UDF
python
# Create a Pandas Series
x = pd.Series([1, 2, 3])
# The function for a pandas_udf executed with local Pandas data
print(cubed(x))
# 输出
0 1
1 8
2 27
dtype: int64
作为 Spark UDF 使用
python
# Create a Spark DataFrame, 'spark' is an existing SparkSession
df = spark.range(1, 4)
# Execute function as a Spark vectorized UDF
df.select("id", cubed_udf(col("id"))).show()
# 输出
+---+---------+
| id|cubed(id)|
+---+---------+
| 1| 1|
| 2| 8|
| 3| 27|
+---+---------+
更多关于 Pandas UDF 的资料可以访问官方文档。
通过 Spark SQL Shell, Beeline 和 Tableau 查询
Spark SQL Shell
spark-sql CLI是执行Spark SQL查询的便捷工具。尽管该实用程序在本地模式下与Hive元数据存储服务通信,但它不会与Thrift JDBC/ODBC服务器(也称为Spark Thrift Server或STS)通信。STS允许JDBC/ODBC客户端通过JDBC和ODBC协议在Apache Spark上执行SQL查询。
在$SPARK_HOME
文件夹中执行以下命令启动 CLI:
python
./bin/spark-sql
创建表
python
spark-sql> CREATE TABLE people (name STRING, age int);
输出内容将包含表所在文件位置 /user/hive/warehouse/people
:
python
20/01/11 22:42:16 WARN HiveMetaStore: Location: file:/user/hive/warehouse/people
specified for non-external table:people
Time taken: 0.63 seconds
插入数据
shell
spark-sql> INSERT INTO people VALUES ("Michael", NULL);
Time taken: 1.696 seconds
spark-sql> INSERT INTO people VALUES ("Andy", 30);
Time taken: 0.744 seconds
spark-sql> INSERT INTO people VALUES ("Samantha", 19);
Time taken: 0.637 seconds
spark-sql>
查询
shell
spark-sql> SELECT name FROM people WHERE age IS NULL;
Michael
Time taken: 0.272 seconds, Fetched 1 row(s)
Beeline
Beeline 是通过 HiveServer2 来运行 HQL 查询数据。
启动 Thrift 服务
在 $SPARK_HOME
下启动:
shell
./sbin/start-thriftserver.sh
连接 Thrift 服务
shell
./bin/beeline
连接成功后显示:
shell
!connect jdbc:hive2://localhost:10000
查询
shell
0: jdbc:hive2://localhost:10000> SHOW tables;
+-----------+------------+--------------+
| database | tableName | isTemporary |
+-----------+------------+--------------+
| default | people | false |
+-----------+------------+--------------+
1 row selected (0.417 seconds)
0: jdbc:hive2://localhost:10000> SELECT * FROM people;
+-----------+-------+
| name | age |
+-----------+-------+
| Samantha | 19 |
| Andy | 30 |
| Michael | NULL |
+-----------+-------+
3 rows selected (1.512 seconds)
0: jdbc:hive2://localhost:10000>
停止 Thrift 服务
shell
./sbin/stop-thriftserver.sh
Tableau
和前两种类似,也需要先启动 Thrift 服务。
关于详细使用可以查看官方文档
外部数据源
可以通过 Spark SQL 查询外部数据源。
通过 JDBC 连接数据库
可以通过类似下边的方式访问数据库:
shell
./bin/spark-shell --driver-class-path $database.jar --jars $database.jar
远程数据库将被加载为 DataFrame 或者 Spark SQL 临时视图。
下边是对一些连接参数的说明:
参数名称 | 说明 |
---|---|
user, password | 连接远程数据库的用户名和密码 |
url | 连接 url, 例如:jdbc:postgresql://localhost/test?user=fred&password=secret |
dbtable | 需要操作的表。 |
query | 查询语句 |
driver | URL 连接中 JDBC 驱动的类名 |
更多参数说明:官方文档
PostgreSQL
启动 pg JDBC jar 包:
shell
bin/spark-shell --jars postgresql-42.2.6.jar
下边展示了如何通过 Spark SQL API 和 JDBC 访问:
scala
// In Scala
// Read Option 1: Loading data from a JDBC source using load method
val jdbcDF1 = spark
.read
.format("jdbc")
.option("url", "jdbc:postgresql:[DBSERVER]")
.option("dbtable", "[SCHEMA].[TABLENAME]")
.option("user", "[USERNAME]")
.option("password", "[PASSWORD]")
.load()
// Read Option 2: Loading data from a JDBC source using jdbc method
// Create connection properties
import java.util.Properties
val cxnProp = new Properties()
cxnProp.put("user", "[USERNAME]")
cxnProp.put("password", "[PASSWORD]")
// Load data using the connection properties
val jdbcDF2 = spark
.read
.jdbc("jdbc:postgresql:[DBSERVER]", "[SCHEMA].[TABLENAME]", cxnProp)
// Write Option 1: Saving data to a JDBC source using save method
jdbcDF1
.write
.format("jdbc")
.option("url", "jdbc:postgresql:[DBSERVER]")
.option("dbtable", "[SCHEMA].[TABLENAME]")
.option("user", "[USERNAME]")
.option("password", "[PASSWORD]")
.save()
// Write Option 2: Saving data to a JDBC source using jdbc method
jdbcDF2.write
.jdbc(s"jdbc:postgresql:[DBSERVER]", "[SCHEMA].[TABLENAME]", cxnProp)
And here's how to do it in PySpark:
通过 PySpark:
python
# In Python
# Read Option 1: Loading data from a JDBC source using load method
jdbcDF1 = (spark
.read
.format("jdbc")
.option("url", "jdbc:postgresql://[DBSERVER]")
.option("dbtable", "[SCHEMA].[TABLENAME]")
.option("user", "[USERNAME]")
.option("password", "[PASSWORD]")
.load())
# Read Option 2: Loading data from a JDBC source using jdbc method
jdbcDF2 = (spark
.read
.jdbc("jdbc:postgresql://[DBSERVER]", "[SCHEMA].[TABLENAME]",
properties={"user": "[USERNAME]", "password": "[PASSWORD]"}))
# Write Option 1: Saving data to a JDBC source using save method
(jdbcDF1
.write
.format("jdbc")
.option("url", "jdbc:postgresql://[DBSERVER]")
.option("dbtable", "[SCHEMA].[TABLENAME]")
.option("user", "[USERNAME]")
.option("password", "[PASSWORD]")
.save())
# Write Option 2: Saving data to a JDBC source using jdbc method
(jdbcDF2
.write
.jdbc("jdbc:postgresql:[DBSERVER]", "[SCHEMA].[TABLENAME]",
properties={"user": "[USERNAME]", "password": "[PASSWORD]"}))
MySQL
启动 Mysql JDBC jar 包:
python
bin/spark-shell --jars mysql-connector-java_8.0.16-bin.jar
通过 Scala 访问:
scala
// In Scala
// Loading data from a JDBC source using load
val jdbcDF = spark
.read
.format("jdbc")
.option("url", "jdbc:mysql://[DBSERVER]:3306/[DATABASE]")
.option("driver", "com.mysql.jdbc.Driver")
.option("dbtable", "[TABLENAME]")
.option("user", "[USERNAME]")
.option("password", "[PASSWORD]")
.load()
// Saving data to a JDBC source using save
jdbcDF
.write
.format("jdbc")
.option("url", "jdbc:mysql://[DBSERVER]:3306/[DATABASE]")
.option("driver", "com.mysql.jdbc.Driver")
.option("dbtable", "[TABLENAME]")
.option("user", "[USERNAME]")
.option("password", "[PASSWORD]")
.save()s
通过 PySpark 访问:
python
# In Python
# Loading data from a JDBC source using load
jdbcDF = (spark
.read
.format("jdbc")
.option("url", "jdbc:mysql://[DBSERVER]:3306/[DATABASE]")
.option("driver", "com.mysql.jdbc.Driver")
.option("dbtable", "[TABLENAME]")
.option("user", "[USERNAME]")
.option("password", "[PASSWORD]")
.load())
# Saving data to a JDBC source using save
(jdbcDF
.write
.format("jdbc")
.option("url", "jdbc:mysql://[DBSERVER]:3306/[DATABASE]")
.option("driver", "com.mysql.jdbc.Driver")
.option("dbtable", "[TABLENAME]")
.option("user", "[USERNAME]")
.option("password", "[PASSWORD]")
.save())
还有一些其他外部数据源的连接使用说明:
高阶函数
当我们对复杂数据类型(多种简单数据类型嵌套)进行操作时,一般有两种方式:
- 拆分成简单的数据类型操作,操作完后合并成复杂类型
- 使用 UDF
Explode 和 Collect
sql
-- In SQL
SELECT id, collect_list(value + 1) AS values
FROM (SELECT id, EXPLODE(values) AS value
FROM table) x
GROUP BY id
EXPLODE 会为values中的每个元素(值)创建一个新的行(带有id)。
然后 collect_list()
会返回一个计算完的列表,需要注意的是 Group BY 需要 shuffle 操作,返回的结果可能并不是想要的顺序,而且 values 列表可能是一个数据很多的很宽的列表,因此计算所消耗的内存是昂贵的。
UDF
上边的查询可以通过 UDF 函数实现:
sql
// In Scala
def addOne(values: Seq[Int]): Seq[Int] = {
values.map(value => value + 1)
}
val plusOneInt = spark.udf.register("plusOneInt", addOne(_: Seq[Int]): Seq[Int])
sql 使用 UDF:
sql
spark.sql("SELECT id, plusOneInt(values) AS values FROM table").show()
虽然使用UDF 返回的查询结果是有序的,但是需要经过序列化和反序列化,资源消耗也是昂贵的。
内置函数
我们可以使用内置高阶函数,进行复杂数据类型的操作,这些内置函数的开销比上边两种都小。
更多请查看官方文档
高阶函数
除了前面提到的内置函数之外,还有一些接受匿名lambda函数作为参数的高阶函数。下面是一个高阶函数的例子:
sql
-- In SQL
transform(values, value -> lambda expression)
transform()函数接受一个数组(值)和一个匿名函数(lambda表达式)作为输入。该函数通过对每个元素应用匿名函数透明地创建一个新数组,然后将结果赋值给输出数组(类似于UDF方法,但效率更高)。
python
# In Python
from pyspark.sql.types import *
schema = StructType([StructField("celsius", ArrayType(IntegerType()))])
t_list = [[35, 36, 32, 30, 40, 42, 38]], [[31, 32, 34, 55, 56]]
t_c = spark.createDataFrame(t_list, schema)
t_c.createOrReplaceTempView("tC")
# Show the DataFrame
t_c.show()
scala
// In Scala
// Create DataFrame with two rows of two arrays (tempc1, tempc2)
val t1 = Array(35, 36, 32, 30, 40, 42, 38)
val t2 = Array(31, 32, 34, 55, 56)
val tC = Seq(t1, t2).toDF("celsius")
tC.createOrReplaceTempView("tC")
// Show the DataFrame
tC.show()
输出内容:
scala
+--------------------+
| celsius|
+--------------------+
|[35, 36, 32, 30, ...|
|[31, 32, 34, 55, 56]|
+--------------------+
transform()
scala
transform(array<T>, function<T, U>): array<U>
transform()函数通过对输入数组的每个元素应用一个函数来生成一个数组(类似于map()函数):
scala
// In Scala/Python
// Calculate Fahrenheit from Celsius for an array of temperatures
spark.sql("""
SELECT celsius,
transform(celsius, t -> ((t * 9) div 5) + 32) as fahrenheit
FROM tC
""").show()
+--------------------+--------------------+
| celsius| fahrenheit|
+--------------------+--------------------+
|[35, 36, 32, 30, ...|[95, 96, 89, 86, ...|
|[31, 32, 34, 55, 56]|[87, 89, 93, 131,...|
+--------------------+--------------------+
filter()
scala
filter(array<T>, function<T, Boolean>): array<T>
filter()函数的作用是:生成一个仅由输入数组中布尔函数为真的元素组成的数组:
scala
// In Scala/Python
// Filter temperatures > 38C for array of temperatures
spark.sql("""
SELECT celsius,
filter(celsius, t -> t > 38) as high
FROM tC
""").show()
+--------------------+--------+
| celsius| high|
+--------------------+--------+
|[35, 36, 32, 30, ...|[40, 42]|
|[31, 32, 34, 55, 56]|[55, 56]|
+--------------------+--------+
exists()
scala
exists(array<T>, function<T, V, Boolean>): Boolean
exists()函数如果布尔函数对输入数组中的任何元素成立则返回true:
scala
// In Scala/Python
// Is there a temperature of 38C in the array of temperatures
spark.sql("""
SELECT celsius,
exists(celsius, t -> t = 38) as threshold
FROM tC
""").show()
+--------------------+---------+
| celsius|threshold|
+--------------------+---------+
|[35, 36, 32, 30, ...| true|
|[31, 32, 34, 55, 56]| false|
+--------------------+---------+
reduce()
scala
reduce(array<T>, B, function<B, T, B>, function<B, R>)
reduce()函数通过使用函数<B, T, B>将数组元素合并到缓冲区B中,并在最后的缓冲区上应用结束函数<B, R>,将数组元素减少为单个值:
scala
// In Scala/Python
// Calculate average temperature and convert to F
spark.sql("""
SELECT celsius,
reduce(
celsius,
0,
(t, acc) -> t + acc,
acc -> (acc div size(celsius) * 9 div 5) + 32
) as avgFahrenheit
FROM tC
""").show()
+--------------------+-------------+
| celsius|avgFahrenheit|
+--------------------+-------------+
|[35, 36, 32, 30, ...| 96|
|[31, 32, 34, 55, 56]| 105|
+--------------------+-------------+
常用 DataFrames 和 Spark SQL 操作
Spark SQL的部分强大功能来自于它支持的广泛的DataFrame操作(也称为无类型数据集操作)。操作列表相当广泛,包括:
- 聚合函数
- 集合函数
- Datetime函数
- 数学函数
- 混合函数
- 非聚合函数
- 排序功能
- 字符串函数
- UDF 函数
- 窗口函数
关于这些函数的具体使用可以查看官方文档
总结
本文探讨了Spark SQL如何与外部组件接口。我们讨论了创建用户定义函数,包括Pandas udf,并提供了一些执行Spark SQL查询的选项(包括Spark SQL shell、Beeline和Tableau)。然后,我们提供了如何使用Spark SQL连接各种外部数据源的示例,如SQL数据库,PostgreSQL, MySQL, Tableau, Azure Cosmos DB, MS SQL Server等。
我们探索了Spark用于复杂数据类型的内置函数,并给出了一些使用高阶函数的示例。最后,我们讨论了一些常见的关系操作符,并展示了如何执行DataFrame操作的选择。