通过 Spark SQL 和 DataFrames 与外部数据源交互

文章目录

前言

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操作的选择。

相关推荐
拓端研究室TRL2 小时前
【梯度提升专题】XGBoost、Adaboost、CatBoost预测合集:抗乳腺癌药物优化、信贷风控、比特币应用|附数据代码...
大数据
黄焖鸡能干四碗2 小时前
信息化运维方案,实施方案,开发方案,信息中心安全运维资料(软件资料word)
大数据·人工智能·软件需求·设计规范·规格说明书
编码小袁2 小时前
探索数据科学与大数据技术专业本科生的广阔就业前景
大数据
WeeJot嵌入式3 小时前
大数据治理:确保数据的可持续性和价值
大数据
zmd-zk4 小时前
kafka+zookeeper的搭建
大数据·分布式·zookeeper·中间件·kafka
激流丶4 小时前
【Kafka 实战】如何解决Kafka Topic数量过多带来的性能问题?
java·大数据·kafka·topic
测试界的酸菜鱼4 小时前
Python 大数据展示屏实例
大数据·开发语言·python
时差9534 小时前
【面试题】Hive 查询:如何查找用户连续三天登录的记录
大数据·数据库·hive·sql·面试·database
Mephisto.java4 小时前
【大数据学习 | kafka高级部分】kafka中的选举机制
大数据·学习·kafka
Mephisto.java4 小时前
【大数据学习 | kafka高级部分】kafka的优化参数整理
大数据·sql·oracle·kafka·json·database