Hive进阶之路

目录

一、Hive 基础回顾

二、数据定义语言(DDL)进阶

2.1 复杂表创建

2.2 分区与分桶优化

三、数据操作语言(DML)深入

3.1 高效的数据加载

3.2 灵活的数据更新与删除

四、Hive 函数高级应用

4.1 内置函数的巧用

4.2 自定义函数(UDF)开发

五、查询优化技巧

5.1 查询语句优化

5.1.1 全表扫描问题

5.1.2 笛卡尔积问题

5.2 执行计划分析

六、Hive 高级特性

6.1 ACID 与事务支持

6.2 Hive on Tez/Spark

6.2.1 Hive 与 Tez 集成

6.2.2 Hive 与 Spark 集成

七、实战案例分析

7.1 电商数据分析案例

7.2 日志分析案例

八、总结与展望


一、Hive 基础回顾

Hive 是基于 Hadoop 的数据仓库工具,能将结构化数据文件映射为数据库表,并提供类 SQL 查询功能,它将 SQL 语句转变成 MapReduce 任务执行,大大降低了大数据处理的门槛,让熟悉 SQL 的开发者能轻松进行数据分析。

Hive 的架构主要包含用户接口、元数据存储、解释器、编译器、优化器和执行器等组件。用户接口常见的有 CLI(命令行接口)、JDBC/ODBC 和 WebGUI,方便用户与 Hive 交互,提交查询语句。元数据存储通常使用关系数据库(如 MySQL),用于保存表结构、列信息、分区、表属性及数据存储目录等元数据,这些元数据对 Hive 理解和处理数据至关重要。当用户提交 Hive SQL 语句后,解释器会将其转换为语法树,编译器把语法树编译成逻辑执行计划,优化器对逻辑执行计划进行优化,以提高执行效率,最后执行器调用底层的运行框架(通常是 MapReduce,也可以是 Tez、Spark 等)执行优化后的逻辑执行计划,并将结果返回给用户。

Hive 与 Hadoop 紧密相关,是 Hadoop 生态系统的重要组成部分。Hadoop 提供了分布式存储(HDFS)和计算(MapReduce)的基础架构,Hive 则构建在 Hadoop 之上。Hive 的数据都存储在 HDFS 中,利用 Hadoop 的分布式存储能力,实现海量数据的存储。在查询执行时,Hive 将 SQL 查询转化为 MapReduce 任务,借助 Hadoop 的 MapReduce 框架进行分布式计算,充分发挥 Hadoop 并行处理大数据的优势。不过,并非所有 Hive 查询都会生成 MapReduce 任务,例如简单的全表扫描(如select * from table)可能无需 MapReduce 即可完成。

在大数据处理领域,Hive 扮演着举足轻重的角色。它为数据仓库的构建和分析提供了强大支持,使企业能方便地对大规模数据进行存储、查询和分析。通过 Hive,数据分析师和开发人员可以利用熟悉的 SQL 语法,完成复杂的数据处理任务,无需深入了解底层复杂的分布式计算和存储细节,大大提高了开发效率和数据分析的灵活性,成为大数据处理不可或缺的工具之一。

二、数据定义语言(DDL)进阶

2.1 复杂表创建

在 Hive 中,除了常见的基本数据类型(如 INT、STRING、DOUBLE 等),还支持复杂数据类型,这使得数据存储和处理更加灵活,能适应多样化的数据格式。

Array 类型:Array 是一种有序的数组类型,用于存储一组相同类型的数据。例如,若要记录每个学生的选修课程列表,就可以使用 Array 类型。建表语句如下:

CREATE TABLE students (

    id INT,

    name STRING,

    courses ARRAY<STRING>

)

ROW FORMAT DELIMITED

FIELDS TERMINATED BY '\t'

COLLECTION ITEMS TERMINATED BY ',';

在这个例子中,courses列的数据类型为ARRAY<STRING>,表示它是一个字符串数组,用于存储学生选修的课程名称。数据文件中,不同课程之间用逗号(由COLLECTION ITEMS TERMINATED BY ','指定)分隔,如下所示:

1    Alice    Math,English,Physics

2    Bob    Chemistry,Biology

Map 类型:Map 是一种键值对的数据结构,用于存储一组键值对,其中键和值的数据类型可以不同。比如,记录每个学生的课程成绩,就可以使用 Map 类型,以课程名称为键,成绩为值。建表语句如下:

CREATE TABLE student_scores (

    id INT,

    name STRING,

    scores MAP<STRING, INT>

)

ROW FORMAT DELIMITED

FIELDS TERMINATED BY '\t'

COLLECTION ITEMS TERMINATED BY ','

MAP KEYS TERMINATED BY ':';

这里,scores列的数据类型为MAP<STRING, INT>,表示它是一个键为字符串(课程名称),值为整数(成绩)的键值对集合。数据文件中,不同键值对之间用逗号分隔,键和值之间用冒号(由MAP KEYS TERMINATED BY ':'指定)分隔,数据如下:

1    Alice    Math:90,English:85,Physics:88

2    Bob    Chemistry:75,Biology:80

Struct 类型:Struct 用于将多个不同类型的数据组合成一个结构体。比如,记录学生的基本信息,包括姓名、年龄和地址,就可以使用 Struct 类型。建表语句如下:

CREATE TABLE student_info (

    id INT,

    info STRUCT<name:STRING, age:INT, address:STRING>

)

ROW FORMAT DELIMITED

FIELDS TERMINATED BY '\t'

COLLECTION ITEMS TERMINATED BY ',';

在这个例子中,info列的数据类型为STRUCT<name:STRING, age:INT, address:STRING>,表示它是一个包含name(字符串类型)、age(整数类型)和address(字符串类型)三个字段的结构体。数据文件中,结构体内部字段之间用逗号分隔,数据如下:

1    Alice,20,New York

2    Bob,22,Los Angeles

这些复杂数据类型在实际应用中非常广泛。在电商领域,记录用户的订单信息时,Array类型可用于存储订单中的商品列表,Map类型可用于存储商品的属性(如颜色、尺码等)及其对应值,Struct类型可用于存储用户的详细地址信息(包括省、市、区等)。在日志分析中,Array类型可用于存储一条日志中的多个操作步骤,Map类型可用于存储日志的各种属性(如时间、来源等)及其对应值,Struct类型可用于存储日志的结构化信息(如用户 ID、设备信息等)。通过合理使用这些复杂数据类型,可以更有效地存储和处理复杂的数据,提高数据分析的效率和灵活性。

2.2 分区与分桶优化

在 Hive 中,分区(Partitioning)和分桶(Bucketing)是两种重要的数据组织和查询优化技术,能显著提升大数据处理的效率。

分区表原理与优势:分区表是根据表的某一列(或多列)的值将数据划分为不同的逻辑分区,每个分区对应 HDFS 上的一个目录,其中存储着符合该分区条件的数据。例如,有一个销售记录表sales,包含order_id(订单 ID)、order_date(订单日期)、customer_id(客户 ID)和amount(订单金额)等字段。如果经常按日期查询销售数据,就可以按order_date列对sales表进行分区,建表语句如下:

CREATE TABLE sales (

    order_id INT,

    customer_id INT,

    amount DECIMAL(10, 2)

)

PARTITIONED BY (order_date STRING);

分区表的优势在于查询时,Hive 只需扫描与查询条件匹配的分区,而无需扫描整个表,大大减少了数据扫描量,从而提高查询效率。比如查询 2023 年 10 月 1 日的销售数据,Hive 只会扫描order_date2023-10-01的分区目录下的数据,而不会扫描其他日期的分区,极大提升了查询速度。此外,分区表在数据管理方面也很方便,如可以方便地删除某个分区的数据,实现数据的快速清理和维护。

分桶表原理与优势:分桶表是使用哈希函数将数据行分配到固定数量的桶(buckets)中,每个桶对应一个数据文件。分桶依据的列称为分桶列,Hive 会对分桶列的值计算哈希值,并根据哈希值将数据分配到相应的桶中。继续以上述sales表为例,如果经常需要按照customer_id进行连接操作,为了提高连接效率,可以按customer_id列对sales表进行分桶,建表语句如下:

CREATE TABLE sales (

    order_id INT,

    order_date STRING,

    customer_id INT,

    amount DECIMAL(10, 2)

)

CLUSTERED BY (customer_id) INTO 16 BUCKETS;

这里将sales表按customer_id列分桶,分为 16 个桶。分桶表在处理连接操作时优势明显。当连接两个分桶表,且它们的分桶列和桶数量相同时,Hive 可以直接根据桶的编号进行连接,而无需扫描整个表,大大减少了数据传输和处理的开销,提高了连接操作的效率。分桶表还能使数据分布更加均匀,避免数据倾斜问题,提升查询性能。

分区与分桶结合使用:在实际应用中,常常将分区和分桶结合起来,以充分发挥两者的优势。比如,对于sales表,可以先按order_date分区,再在每个分区内按customer_id分桶,建表语句如下:

CREATE TABLE sales (

    order_id INT,

    customer_id INT,

    amount DECIMAL(10, 2)

)

PARTITIONED BY (order_date STRING)

CLUSTERED BY (customer_id) INTO 16 BUCKETS;

这样,查询时既能利用分区快速定位到相关日期的数据,又能利用分桶提高连接等操作的效率,进一步提升了查询性能和数据处理的灵活性。例如,在分析不同日期不同客户的销售数据时,这种方式能快速准确地获取所需数据,满足复杂的数据分析需求。通过合理使用分区和分桶技术,可以有效优化 Hive 的数据存储和查询性能,提高大数据处理的效率。

三、数据操作语言(DML)深入

3.1 高效的数据加载

在 Hive 中,数据加载是将数据导入到 Hive 表的重要操作,不同的数据加载方式适用于不同的场景,了解它们的差异和适用场景,能帮助我们更高效地处理数据。

LOAD DATA:这是最常用的数据加载方式,语法为LOAD DATA [LOCAL] INPATH 'filepath' [OVERWRITE] INTO TABLE tablename [PARTITION (partcol1=val1, partcol2=val2 ...)]LOCAL关键字表示从本地文件系统导入数据,若不指定,则从 HDFS 文件系统导入,此时是文件的移动操作,类似剪切。OVERWRITE关键字表示执行数据覆盖操作,原有数据会被全部覆盖(若是分区表,则覆盖指定分区);若不指定,则执行数据追加操作,原有数据不会被覆盖。比如,有一个本地的 CSV 文件employees.csv,内容如下:

1,Alice,30,HR

2,Bob,25,Engineering

3,Charlie,35,Sales

创建一个 Hive 表并加载该文件的语句如下:

CREATE TABLE employees (

    id INT,

    name STRING,

    age INT,

    department STRING

)

ROW FORMAT DELIMITED

FIELDS TERMINATED BY ','

STORED AS TEXTFILE;

LOAD DATA LOCAL INPATH '/path/to/employees.csv' INTO TABLE employees;

LOAD DATA方式适合将大量的本地文件或 HDFS 文件快速加载到 Hive 表中,因为它直接移动或复制文件,不进行数据解析和转换,效率较高。但使用时需注意,它不会检查表的字段,若文件中的字段多了,会自动舍去,少了则会使用 NULL 填充,所以在使用前需确保对表结构和文件数据非常熟悉。

INSERT INTO:用于将查询结果插入到 Hive 表中,语法为INSERT INTO TABLE tablename [PARTITION (partcol1=val1, partcol2=val2 ...)] SELECT ...。例如,有两个表source_tabletarget_table,将source_table中的数据插入到target_table中的语句如下:

CREATE TABLE source_table (

    id INT,

    name STRING,

    age INT,

    department STRING

)

ROW FORMAT DELIMITED

FIELDS TERMINATED BY ','

STORED AS TEXTFILE;

CREATE TABLE target_table (

    id INT,

    name STRING,

    age INT,

    department STRING

)

ROW FORMAT DELIMITED

FIELDS TERMINATED BY ','

STORED AS TEXTFILE;

INSERT INTO TABLE target_table SELECT * FROM source_table;

INSERT INTO适用于将一个表的查询结果插入到另一个表中,或者将复杂查询的结果保存到表中。与LOAD DATA不同,它允许从一个表动态地导入数据,并且可以在插入过程中对数据进行处理和转换,比如进行字段计算、过滤等操作。但由于它会对查询结果进行处理,所以在处理大规模数据时,性能可能不如LOAD DATA

CREATE TABLE AS SELECT (CTAS):通过查询结果创建新表并插入数据,语法为CREATE TABLE new_table AS SELECT ...。假设已有source_table,希望根据查询结果创建一个新表new_table,包含source_table中年龄大于 30 的记录,语句如下:

CREATE TABLE source_table (

    id INT,

    name STRING,

    age INT,

    department STRING

)

ROW FORMAT DELIMITED

FIELDS TERMINATED BY ','

STORED AS TEXTFILE;

CREATE TABLE new_table AS SELECT id, name, age FROM source_table WHERE age > 30;

CTAS适用于根据特定的查询结果创建新表,并将结果数据存储到新表中。它在创建新表时,会根据查询结果自动推断表的结构,无需手动指定列的数据类型等信息,使用起来非常方便。不过,由于它会创建新表,所以在数据量较大时,会占用较多的存储空间。

在实际应用中,选择合适的数据加载方式至关重要。若要加载大量的本地文件或 HDFS 文件,且对数据处理要求不高,可优先使用LOAD DATA;若需要将查询结果保存到表中,或对数据进行处理和转换后再插入,可使用INSERT INTO;若要根据查询结果创建新表并存储数据,则CREATE TABLE AS SELECT是较好的选择。同时,为了提高数据加载的效率,还可以采取一些优化措施,如在加载数据前对数据文件进行预处理,确保数据格式正确、字段匹配,避免数据加载过程中的错误和性能损耗。合理使用这些数据加载方式,能有效提升 Hive 数据处理的效率和灵活性。

3.2 灵活的数据更新与删除

在传统数据库中,数据更新(UPDATE)和删除(DELETE)操作是非常常见且方便的,可直接使用UPDATEDELETE语句对数据进行修改和删除。但 Hive 在默认情况下,由于其基于 Hadoop 的架构和 HDFS 的特性(一次写入,多次读取),对数据更新和删除操作的支持相对有限,实现方式与传统数据库有所不同。

Hive 的数据更新操作:Hive 从 0.14 版本开始支持事务和行级更新,但默认是不支持的,需要进行一些附加配置。要使 Hive 支持行级insertupdatedelete,需配置 Hive 支持事务。具体配置如下:

<property>

    <name>hive.support.concurrency</name>

    <value>true</value>

</property>

<property>

    <name>hive.exec.dynamic.partition.mode</name>

    <value>nonstrict</value>

</property>

<property>

    <name>hive.txn.manager</name>

    <value>org.apache.hadoop.hive.ql.lockmgr.DbTxnManager</value>

</property>

<property>

    <name>hive.compactor.initiator.on</name>

    <value>true</value>

</property>

<property>

    <name>hive.compactor.worker.threads</name>

    <value>1</value>

</property>

并且,要执行Update的表需满足特定语法要求:建表时必须带有buckets(分桶)属性;需指定格式,目前只支持ORCFileformatAcidOutputFormat;建表时必须指定参数('transactional' = true)。例如,创建一个支持更新操作的表:

CREATE TABLE t2 (

    id INT,

    name STRING

)

PARTITIONED BY (country STRING, state STRING)

CLUSTERED BY (id) INTO 8 BUCKETS

STORED AS ORC

TBLPROPERTIES ('transactional' = 'true');

然后可以进行更新操作,如UPDATE t2 SET name='张' WHERE id=1;。不过,Hive 的更新机制速度相对较慢,在小数据集上更新也可能需要分钟级别,在大数据量场景下,性能问题更为突出,基本上处于不太可用的状态。在实际应用中,若要更新数据,一种常用的替代方法是将要更新的数据导出到一个新表,然后将更新后的数据再导入原表。例如:

-- 创建一个新表来存放更新后的数据

CREATE TABLE new_table (

    id INT,

    name STRING,

    age INT

);

-- 更新数据,假设更新id为1的数据

INSERT OVERWRITE TABLE new_table

SELECT id, 'new_name', age

FROM original_table

WHERE id = 1;

-- 将更新后的数据导入原表

INSERT OVERWRITE TABLE original_table

SELECT nt.id, nt.name, nt.age

FROM new_table nt

LEFT JOIN original_table ot

ON nt.id = ot.id;

Hive 的数据删除操作:同样,在配置好事务支持后,可以使用DELETE语句进行删除操作,如DELETE FROM t2 WHERE name='李四';。但与更新操作类似,直接使用DELETE语句在大数据量下性能不佳。通常也采用类似的间接方法来实现删除功能,即创建一个新表,将要保留的数据插入新表,然后用新表数据覆盖原表数据。例如:

-- 创建一个新表来存放要保留的数据

CREATE TABLE new_table (

    id INT,

    name STRING,

    age INT

);

-- 删除数据,假设删除id为1的数据

INSERT OVERWRITE TABLE new_table

SELECT id, name, age

FROM original_table

WHERE id <> 1;

-- 将新表数据导入原表

INSERT OVERWRITE TABLE original_table

SELECT nt.id, nt.name, nt.age

FROM new_table nt;

适用场景:尽管 Hive 原生的更新和删除操作存在性能上的不足,但在一些特定场景下仍有应用价值。在流式接收数据场景中,事务支持可获得数据的一致性视图,避免产生过多小文件给 NameNode 造成压力;在缓慢变化维场景中,如维度表随时间缓慢变化,可使用行级更新操作;在数据重述场景中,当发现数据集合有错误需要更正,或业务规则需要根据后续事务重述特定事务时,也可使用更新和删除操作。而在大数据量的常规数据处理中,通过导出数据到新表再导入的间接方式,虽然操作繁琐,但能在一定程度上满足数据更新和删除的需求,也是目前较为可行的解决方案。

四、Hive 函数高级应用

4.1 内置函数的巧用

Hive 提供了丰富的内置函数,涵盖日期、字符串、数学等多个领域,熟练掌握这些函数的高级用法,能在复杂业务场景中高效处理数据。

日期函数:在处理时间序列数据时,日期函数非常实用。current_date()用于获取当前日期,格式为yyyy-MM-ddcurrent_timestamp()则返回当前系统时间,精确到毫秒,格式为yyyy-MM-dd HH:mm:ss。例如,在电商业务中,要统计每天的订单数量,可结合current_date()group by语句实现:

SELECT current_date() AS today, COUNT(*) AS order_count

FROM orders

GROUP BY current_date();

unix_timestamp([STRING timestamp [, STRING pattern]])函数返回时间对应的 Unix 时间戳(即距离1970-01-01 00:00:00的秒数),若不传入参数则返回当前时间的 Unix 时间戳。与之对应的from_unixtime(BIGINT unixtime [, STRING format])函数将 Unix 时间戳转换为指定格式的时间。比如,将时间戳1609459200转换为yyyy-MM-dd格式的日期:

SELECT from_unixtime(1609459200, 'yyyy-MM-dd');

date_add(DATE startdate, INT days)date_sub(DATE startdate, INT days)分别用于对时间按天做加减,datediff(STRING enddate, STRING startdate)用于计算两个时间相差的天数。在分析用户活跃周期时,可能会用到这些函数,如计算用户首次登录和最近一次登录的时间间隔:

SELECT user_id,

       datediff(max(last_login_time), min(first_login_time)) AS active_days

FROM user_login_log

GROUP BY user_id;

字符串函数:字符串函数在处理文本数据时不可或缺。concat(string A, string B…)用于拼接字符串,concat_ws(string SEP, string A, string B…)则在拼接时可指定分隔符。例如,将用户的姓和名拼接成完整姓名,并以空格分隔:

SELECT concat_ws(' ', first_name, last_name) AS full_name

FROM users;

substr(string A, int start, int len)用于截取字符串,从指定位置start开始,截取长度为len的子串。在处理 URL 时,可能需要截取特定部分,如从https://www.example.com/path/to/page中截取路径部分:

SELECT substr('https://www.example.com/path/to/page', 20);

regexp_replace(string x, string y, string z)用于替换字符串中的指定字段,将字符串x中的y替换为z。比如,将文本中的敏感词替换为***

SELECT regexp_replace('This is a secret message', 'secret', '***') AS censored_message;

数学函数:数学函数在数值计算和统计分析中发挥重要作用。round(double a, int d)用于对数字进行四舍五入,保留d位小数。在计算商品平均价格时,可使用该函数保留两位小数:

SELECT round(AVG(price), 2) AS average_price

FROM products;

ceil(double a)floor(double a)分别用于向上取整和向下取整。例如,在计算订单数量时,若要确保订单数量为整数,可使用ceil函数:

SELECT ceil(SUM(quantity)) AS total_quantity

FROM order_items;

power(double a, int n)用于求指定an次方。在计算复利时,会用到该函数,假设年利率为r,存款年限为n,初始本金为P,计算最终本息和:

SELECT P * power((1 + r), n) AS final_amount

FROM investment;

4.2 自定义函数(UDF)开发

当 Hive 内置函数无法满足业务需求时,就需要开发自定义函数(UDF)。下面以 Java 语言为例,讲解 UDF 的开发步骤。

创建 Java 类:首先,创建一个 Maven 项目,并在pom.xml文件中添加 Hive 和 Hadoop 相关依赖,以便在项目中使用 Hive 的 API。例如:

<dependencies>

    <dependency>

        <groupId>org.apache.hive</groupId>

        <artifactId>hive-exec</artifactId>

        <version>3.1.2</version>

    </dependency>

    <dependency>

        <groupId>org.apache.hadoop</groupId>

        <artifactId>hadoop-common</artifactId>

        <version>3.3.1</version>

    </dependency>

</dependencies>

然后,创建一个继承自org.apache.hadoop.hive.ql.exec.UDF的 Java 类,并重写evaluate方法。例如,开发一个将字符串转为大写的 UDF:

package com.example.udf;

import org.apache.hadoop.hive.ql.exec.UDF;

import org.apache.hadoop.io.Text;

public class StringToUpper extends UDF {

    public Text evaluate(Text input) {

        if (input == null) {

            return null;

        }

        return new Text(input.toString().toUpperCase());

    }

}

打包部署:在项目根目录下执行mvn clean package命令,将项目打包成 JAR 文件。将生成的 JAR 文件上传到 Hive 服务器上。可以使用hdfs dfs -put命令将 JAR 文件上传到 HDFS 上,如:

hdfs dfs -put /local/path/to/your-udf.jar /hive/udf/

在 Hive 中使用:在 Hive 客户端中,使用add jar命令将上传的 JAR 文件添加到 Hive 的类路径中,如:

add jar hdfs:///hive/udf/your-udf.jar;

接着,使用CREATE TEMPORARY FUNCTION语句创建临时函数,将 Java 类与 Hive 函数关联起来,如:

CREATE TEMPORARY FUNCTION string_to_upper AS 'com.example.udf.StringToUpper';

现在就可以在 Hive 查询中使用自定义函数了,例如:

SELECT string_to_upper('hello world') AS upper_string;

若要创建永久函数,可使用CREATE FUNCTION语句,并指定 JAR 文件的 HDFS 路径,如:

CREATE FUNCTION my_db.string_to_upper AS 'com.example.udf.StringToUpper'

USING JAR 'hdfs:///hive/udf/your-udf.jar';

解决特定业务问题:以解析 JSON 数据为例,假设 Hive 表中存储的是 JSON 格式的用户信息,需要提取其中的某个字段。Hive 内置的get_json_object函数每次只能返回一个数据项,若要提取多个字段,使用起来不太方便。此时,可以开发一个自定义 UDF 来解析 JSON 数据。例如:

package com.example.udf;

import org.apache.hadoop.hive.ql.exec.UDF;

import org.apache.hadoop.io.Text;

import org.json.JSONObject;

public class JsonExtractor extends UDF {

    public Text evaluate(Text jsonText, Text field) {

        if (jsonText == null || field == null) {

            return null;

        }

        try {

            JSONObject jsonObject = new JSONObject(jsonText.toString());

            return new Text(jsonObject.getString(field.toString()));

        } catch (Exception e) {

            return null;

        }

    }

}

按照上述步骤打包部署并在 Hive 中创建函数后,就可以使用该 UDF 提取 JSON 数据中的字段了,如:

SELECT json_extractor(json_data, 'name') AS user_name

FROM user_info_table;

通过开发和使用自定义函数,能够根据具体业务需求,灵活扩展 Hive 的功能,解决复杂的数据处理问题。

五、查询优化技巧

5.1 查询语句优化

在 Hive 查询中,全表扫描和笛卡尔积是常见的导致查询性能低下的问题,深入理解它们并掌握优化策略至关重要。

5.1.1 全表扫描问题

全表扫描是指在查询时对整个表的数据进行遍历读取,这在数据量较大时会消耗大量的时间和资源。例如,有一个包含数十亿条记录的销售记录表sales,表结构如下:

CREATE TABLE sales (

    id INT,

    order_date STRING,

    customer_id INT,

    product_id INT,

    amount DECIMAL(10, 2)

)

ROW FORMAT DELIMITED

FIELDS TERMINATED BY ','

STORED AS TEXTFILE;

若执行简单的查询语句SELECT * FROM sales;,Hive 会对sales表进行全表扫描,读取每一条记录,这在大数据量下效率极低。

优化策略

a. 添加过滤条件:在WHERE子句中添加过滤条件,可显著减少扫描的数据量。比如查询 2023 年 10 月的销售记录,可使用SELECT * FROM sales WHERE order_date BETWEEN '2023-10-01' AND '2023-10-31';,这样 Hive 只会扫描符合日期条件的记录,大大提高查询效率。

b. 使用分区表:如前文所述,按order_datesales表进行分区,查询时 Hive 只需扫描指定日期分区的数据。例如:

CREATE TABLE sales (

    id INT,

    customer_id INT,

    product_id INT,

    amount DECIMAL(10, 2)

)

PARTITIONED BY (order_date STRING);

查询时SELECT * FROM sales WHERE order_date = '2023-10-01';,Hive 仅扫描order_date2023-10-01的分区,避免了全表扫描。

c. 利用索引:虽然 Hive 的索引机制不像传统关系数据库那样强大,但在特定场景下仍可提高查询性能。在频繁查询的列上创建索引,如对customer_id列创建索引:

CREATE INDEX idx_customer_id ON TABLE sales (customer_id);

这样在查询特定customer_id的销售记录时,可利用索引快速定位数据,减少扫描范围。

5.1.2 笛卡尔积问题

笛卡尔积是指在查询时,将两个或多个表的每一行进行组合,产生的结果集行数是各个表行数的乘积,这会导致数据量急剧膨胀,查询效率极低。例如,有两个表customersorders,表结构如下:

CREATE TABLE customers (

    customer_id INT,

    customer_name STRING,

    address STRING

)

ROW FORMAT DELIMITED

FIELDS TERMINATED BY ','

STORED AS TEXTFILE;

CREATE TABLE orders (

    order_id INT,

    customer_id INT,

    order_date STRING,

    amount DECIMAL(10, 2)

)

ROW FORMAT DELIMITED

FIELDS TERMINATED BY ','

STORED AS TEXTFILE;

若执行错误的查询语句SELECT * FROM customers, orders;,Hive 会生成笛卡尔积,假设customers表有 1000 条记录,orders表有 500 条记录,那么结果集将有 500,000 条记录,处理如此庞大的结果集将耗费大量资源。

优化策略

a. 添加关联条件:在JOIN操作中,确保添加正确的关联条件,避免生成笛卡尔积。正确的查询应该是SELECT * FROM customers JOIN orders ON customers.customer_id = orders.customer_id;,这样 Hive 会根据关联条件进行连接操作,而不是生成笛卡尔积。

b. 小表驱动大表:当一个小表和一个大表进行JOIN时,使用小表驱动大表的方式,可减少数据传输和处理的开销。例如:

SELECT /*+ MAPJOIN(customers) */ *

FROM orders

JOIN customers ON orders.customer_id = customers.customer_id;

这里使用MAPJOIN提示,将customers小表加载到内存中,在Map阶段就完成连接操作,避免在Reduce阶段进行大规模的数据传输和处理,提高查询效率。

c. 避免不必要的JOIN:仔细检查查询需求,避免进行不必要的表连接操作,减少数据处理量。若只需要orders表的部分数据,就无需与customers表进行连接,直接查询orders表即可,如SELECT order_id, order_date, amount FROM orders;

5.2 执行计划分析

在 Hive 中,使用EXPLAIN命令可以查看查询执行计划,这对于理解查询的执行过程和优化查询性能非常有帮助。

使用EXPLAIN命令EXPLAIN命令的基本语法为EXPLAIN [EXTENDED|CBO|AST|DEPENDENCY|AUTHORIZATION|LOCKS|VECTORIZATION|ANALYZE] query,其中query是要分析的 Hive 查询语句,EXTENDED等参数是可选的,用于显示不同类型的详细信息。例如,对于查询语句SELECT * FROM sales WHERE order_date = '2023-10-01';,使用EXPLAIN命令查看执行计划:

EXPLAIN SELECT * FROM sales WHERE order_date = '2023-10-01';

执行上述命令后,会输出查询的执行计划信息,主要包括以下几个部分:

抽象语法树(部分版本已移除,可使用单独命令查看):展示查询语句的语法结构,帮助理解查询的逻辑组成。

Stage Dependencies:各个阶段之间的依赖关系,显示查询执行过程中不同阶段的先后顺序和依赖情况。例如,可能存在多个StageStage-1可能依赖Stage-2,表示Stage-2必须先执行完成,Stage-1才能开始执行。

Stage Plan:各个阶段的执行计划,详细描述每个阶段的具体操作,如Map Operator TreeReduce Operator Tree中的操作。Map Operator Tree描述Map阶段的操作,Reduce Operator Tree描述Reduce阶段的操作。

解读执行计划各部分含义

TableScan:表扫描操作,通常是Map端的第一个操作,用于读取表中的数据。alias表示表的别名,Statistics包含表的统计信息,如数据行数、数据大小等。

Select Operator:选取操作,用于选择查询结果中的列。expressions表示选取的列及其数据类型,outputColumnNames表示输出的列名称。

Filter Operator:过滤操作,根据predicate中的条件对数据进行过滤。例如,predicate: (order_date = '2023-10-01')表示根据order_date等于2023-10-01的条件进行过滤。

Group By Operator:分组聚合操作,aggregations显示聚合函数信息,如sumcount等,keys表示分组的列。

Join Operator:连接操作,condition map表示连接方式(如InnerJoinLeftJoin等),keys表示连接条件字段,outputColumnNames表示连接后输出的字段。

根据执行计划优化查询:通过分析执行计划,可以发现查询中的性能瓶颈,并针对性地进行优化。如果执行计划中显示存在全表扫描,可通过添加过滤条件、使用分区表或索引等方式进行优化。若执行计划中Join操作的MapReduce阶段数据量过大,可考虑使用MapJoin优化,将小表加载到内存中进行连接。例如,对于一个包含多个表连接的复杂查询,执行计划显示在Reduce阶段出现数据倾斜,导致查询性能低下。进一步分析发现,是由于连接键分布不均匀造成的。此时,可以通过调整连接顺序,将数据分布较均匀的表放在前面进行连接,或者对连接键进行预处理,如对空值赋予随机值,使其分布更均匀,从而优化查询性能。通过深入分析执行计划,能更好地理解 Hive 查询的执行过程,找到性能瓶颈,采取有效的优化措施,提高查询效率。

六、Hive 高级特性

6.1 ACID 与事务支持

在大数据处理领域,随着应用场景的不断拓展,对数据一致性和事务处理的需求日益增长。Hive 从 0.13 版本之后开始支持 ACID 事务特性,并在后续版本中逐步完善,这使得 Hive 在处理数据时能够更好地满足复杂业务场景的要求。

ACID 是数据库事务正确执行的四个基本要素的缩写,分别代表原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。在 Hive 中,这些特性的实现对于保证数据的可靠性和一致性至关重要。

原子性:确保每个事务都是一个原子操作,要么完全执行,要么完全不执行。Hive 通过使用事务日志(如 HDFS 的事务文件和合并文件)来支持原子性,确保所有变更要么全部成功,要么全部失败。例如,在一个包含多条插入语句的事务中,如果其中一条插入语句失败,整个事务将回滚,不会有部分数据被插入到表中。

一致性:保证事务执行前后,数据库的状态始终是有效的,并且符合定义的约束条件。Hive 通过事务日志、数据的校验以及约束条件(如 PRIMARY KEY 等)来实现一致性。事务操作必须在数据表中定义合适的字段来进行完整性检查。比如,在一个订单表中,订单金额必须大于 0,当执行更新订单金额的事务时,Hive 会检查新的金额是否符合这个约束条件,若不符合则事务不会提交,从而保证数据的一致性。

隔离性:确保在多个并发事务的情况下,每个事务的执行结果对其他事务不可见,直到事务提交。Hive 通过在底层使用行级锁和分布式锁(如 HBase 或其他支持事务的存储系统)来确保隔离性。Hive 事务的隔离级别默认为 READ COMMITTED,这意味着一个事务只能看到已提交事务的结果。例如,当多个用户同时对一个用户信息表进行更新操作时,每个用户的事务在提交前,其他用户无法看到其修改的内容,避免了数据的不一致和干扰。

持久性:确保一旦事务提交,其结果就永久保存,即使系统发生崩溃,数据也不会丢失。Hive 的持久性是通过将事务日志记录到 HDFS 中来实现的。即便发生硬件故障,日志文件可以帮助恢复数据。比如,当一个事务成功提交后,相关的事务日志会被写入 HDFS,即使此时服务器突然断电,在恢复后也可以根据日志文件将数据恢复到事务提交后的状态。

Hive 事务支持适用于多种场景。在流式接收数据场景中,许多用户使用如 Apache Flume、Apache Storm 或 Apache Kafka 等工具将流数据灌入 Hadoop 集群。当这些工具以每秒数百行的频率写入时,Hive 也许只能每 15 分钟到 1 小时添加一个分区,因为过于频繁地添加分区很快就会使一个表中的分区数量难以维护。而且这些工具还可能向已存在的分区中写数据,但是这样将会产生脏读(可能读到查询开始时间点以后写入的数据),还在这些分区的所在目录中遗留大量小文件,进而给 NameNode 造成压力。在这个使用场景下,事务支持可以获得数据的一致性视图同时避免产生过多的文件。在缓慢变化维场景中,在一个典型的星型模式数据仓库中,维度表随时间的变化很缓慢。例如,一个零售商开了一家新商店,需要将新店数据加到商店表,或者一个已有商店的营业面积或其它需要跟踪的特性改变了。这些改变会导致插入或修改个别记录,从 0.14 版本开始,Hive 支持行级更新,能够很好地满足这种需求。

要启用 Hive 的事务支持,需要进行一系列配置。在 hive-site.xml 中添加以下配置,启用事务支持:

<property>

    <name>hive.support.concurrency</name>

    <value>true</value>

    <description>Enable support for ACID transactions in Hive.</description>

</property>

<property>

    <name>hive.enforce.bucketing</name>

    <value>true</value>

    <description>Enforce bucketing for ACID tables.</description>

</property>

<property>

    <name>hive.exec.dynamic.partition.mode</name>

    <value>nonstrict</value>

    <description>Allow dynamic partitioning in transactions.</description>

</property>

<property>

    <name>hive.txn.manager</name>

    <value>org.apache.hadoop.hive.ql.lockmgr.DbTxnManager</value>

    <description>Specify the transaction manager for Hive (DbTxnManager supports transactions).</description>

</property>

<property>

    <name>hive.compactor.initiator.on</name>

    <value>true</value>

    <description>Enable the compaction process for ACID tables.</description>

</property>

<property>

    <name>hive.compactor.worker.threads</name>

    <value>1</value>

    <description>Number of threads used for compaction of tables.</description>

</property>

<property>

    <name>hive.acid.output.format</name>

    <value>org.apache.hadoop.hive.ql.io.AcidOutputFormat</value>

    <description>Enable acid output format for transactional tables.</description>

</property>

Hive 的 ACID 事务只支持 ORC(Optimized Row Columnar)格式的表。因此,创建支持 ACID 事务的表时,需要指定 STORED AS ORC,例如:

CREATE TABLE transaction_table (

    id INT,

    name STRING,

    amount DOUBLE

)

CLUSTERED BY (id) INTO 4 BUCKETS

STORED AS ORC

TBLPROPERTIES ('transactional' = 'true');

Hive 的事务支持与传统数据库事务存在一些异同。相同点在于,两者都遵循 ACID 原则,致力于保证数据的一致性、完整性和可靠性。不同点则较为明显,传统数据库通常基于单机或小型集群架构,其事务处理能力强大,能高效处理大量并发事务,并且支持复杂的事务嵌套和回滚操作。而 Hive 构建于 Hadoop 分布式架构之上,主要面向大数据批处理,虽然引入了 ACID 特性,但事务处理性能相对较弱,不太适合高并发的实时事务处理场景。在处理复杂事务时,传统数据库有着成熟的并发控制机制和高效的存储引擎支持,而 Hive 在事务处理过程中,由于涉及分布式存储和计算,会产生较高的网络开销和延迟。不过,Hive 的优势在于其能够处理海量数据,通过分布式计算框架实现数据的并行处理,在大数据分析领域有着广泛的应用。

6.2 Hive on Tez/Spark

在 Hive 的生态系统中,执行引擎的选择对查询性能和资源利用有着至关重要的影响。Tez 和 Spark 作为两种高效的执行引擎,与 Hive 的集成能够显著提升 Hive 的性能和功能。

6.2.1 Hive 与 Tez 集成

Tez 是一个基于 Hadoop YARN 的分布式计算框架,它对 MapReduce 进行了优化,旨在提供更高效、更灵活的计算模型。Hive 与 Tez 集成后,能够充分利用 Tez 的优势,提升查询执行效率。

优势:Tez 允许更细粒度的任务划分和更灵活的任务依赖关系,能够优化复杂查询的执行计划。在一个包含多个 JOIN 和聚合操作的复杂查询中,Tez 可以根据数据的特点和查询逻辑,将任务进行合理拆分和调度,避免不必要的中间数据传输和计算,从而大大提高查询性能。Tez 在处理复杂 SQL 时,内部翻译 SQL 能实现任意的 Map、Reduce、Reduce 组合,而 MapReduce 只能 Map-Reduce-Map-Reduce,因此 Tez 在执行复杂 SQL 时优势明显。Tez 在执行查询时,还能提供动态的进度指示,让用户更直观地了解查询的执行状态。

配置和使用方法:要将 Hive 配置为使用 Tez 作为执行引擎,需要在 hive-site.xml 中进行相应配置。添加以下配置:

<property>

    <name>hive.execution.engine</name>

    <value>tez</value>

</property>

如果需要指定 Tez 库的位置,还可以添加:

<property>

    <name>tez.lib.uris</name>

    <value>hdfs:///path/to/tez.tar.gz</value>

</property>

配置完成后,在 Hive 会话中即可使用 Tez 执行查询。可以通过 SET 语句临时设置执行引擎为 Tez:

SET hive.execution.engine=tez;

然后执行查询,如:

SELECT * FROM sales WHERE order_date = '2023-10-01';

6.2.2 Hive 与 Spark 集成

Spark 是一个快速、通用的数据处理引擎,具有内存计算的特性,能够显著提升数据处理的速度。Hive 与 Spark 集成后,用户可以利用 Spark 的高性能计算能力来执行 Hive 查询。

优势:Spark 的内存计算特性使得数据可以在内存中进行处理,避免了频繁的磁盘 I/O 操作,大大提高了查询的响应速度。在进行实时数据分析时,Spark 能够快速处理大量数据,并及时返回结果,满足实时性要求。Spark 还支持多种编程语言,如 Scala、Java、Python 等,这使得开发者可以根据自己的需求选择合适的语言进行开发。

配置和使用方法:配置 Spark 以使用 Hive,首先要确保系统中安装了 Hadoop、Hive 和 Spark。然后设置 Hadoop 和 Hive 环境变量,例如:

export HADOOP_HOME=/path/to/hadoop

export HIVE_HOME=/path/to/hive

export SPARK_HOME=/path/to/spark

在 Spark 的 conf/spark-defaults.conf 中添加 Hive 支持的配置,如:

spark.sql.hive.metastore.version=1.2.1

spark.sql.hive.metastore.jars=local

spark.sql.hive.thriftServer=off

如果使用 SparkSession,可以通过以下方式启用 Hive 支持:

from pyspark.sql import SparkSession

spark = SparkSession.builder \

   .appName("Spark Hive Example") \

   .config("spark.sql.warehouse.dir", "/path/to/spark-warehouse") \

   .enableHiveSupport() \

   .getOrCreate()

然后就可以使用 Spark 执行 Hive 查询,如:

df = spark.sql("SELECT * FROM employee")

df.show()

不同执行引擎性能对比:在实际应用中,Tez 和 Spark 在性能上各有优势。Tez 在处理复杂查询时,通过优化任务调度和执行计划,能够有效减少中间数据的传输和计算,提高查询效率。而 Spark 由于其内存计算的特性,在处理迭代计算和实时查询时表现出色。例如,在一个需要多次迭代计算的机器学习算法应用中,Spark 可以将中间结果保存在内存中,避免了重复读取磁盘数据,大大加快了计算速度。而在处理复杂的 ETL 任务,涉及多个表的复杂关联和聚合操作时,Tez 可能会因为其灵活的任务调度和执行计划优化,展现出更好的性能。在选择执行引擎时,需要根据具体的业务场景和数据特点进行评估和选择。如果业务以复杂的批处理查询为主,Tez 可能是更好的选择;如果需要进行实时数据分析或迭代计算,Spark 则更具优势。

七、实战案例分析

7.1 电商数据分析案例

在电商领域,海量的交易数据蕴含着丰富的商业价值,Hive 作为强大的数据处理工具,能够高效地对这些数据进行分析,为企业决策提供有力支持。以下以一个电商数据为例,展示 Hive 在实际业务中的应用,包括数据清洗、指标计算和报表生成。

假设我们有一份电商交易数据,存储在 HDFS 上的文件中,数据包含以下字段:order_id(订单 ID)、user_id(用户 ID)、product_id(产品 ID)、order_date(订单日期)、quantity(购买数量)、price(产品单价)、payment_amount(支付金额)。

数据清洗:原始数据中可能存在各种问题,如数据缺失、重复记录、异常值等,需要进行清洗。首先,检查数据完整性,删除包含空值的记录。在 Hive 中,可以使用如下语句:

-- 创建一个新表用于存储清洗后的数据

CREATE TABLE clean_orders AS

SELECT *

FROM raw_orders

WHERE order_id IS NOT NULL

  AND user_id IS NOT NULL

  AND product_id IS NOT NULL

  AND order_date IS NOT NULL

  AND quantity IS NOT NULL

  AND price IS NOT NULL

  AND payment_amount IS NOT NULL;

然后,检查并删除重复记录,可根据order_id判断记录是否重复 :

-- 创建临时表存储去重后的数据

CREATE TABLE temp_orders AS

SELECT DISTINCT *

FROM clean_orders;

-- 用临时表覆盖原表,完成去重

DROP TABLE clean_orders;

ALTER TABLE temp_orders RENAME TO clean_orders;

还要处理异常值,如quantityprice不能为负数。对于quantity为负数的情况,可以将其设置为 0(假设是数据录入错误导致):

UPDATE clean_orders

SET quantity = CASE

                   WHEN quantity < 0 THEN 0

                   ELSE quantity

                   END;

对于price为负数的情况,同样可以将其设置为 0 :

UPDATE clean_orders

SET price = CASE

                WHEN price < 0 THEN 0

                ELSE price

                END;

指标计算:清洗数据后,可计算各种业务指标。

计算每日订单总数

SELECT order_date, COUNT(*) AS order_count

FROM clean_orders

GROUP BY order_date;

计算每个用户的总购买金额

SELECT user_id, SUM(payment_amount) AS total_payment

FROM clean_orders

GROUP BY user_id;

计算每个产品的销售总量

SELECT product_id, SUM(quantity) AS total_quantity

FROM clean_orders

GROUP BY product_id;

报表生成:为了更直观地展示数据分析结果,可将指标数据生成报表。假设要生成每日订单统计报表,以 CSV 格式保存到 HDFS 上。首先,使用 Hive 查询获取每日订单统计数据:

SELECT order_date, COUNT(*) AS order_count

FROM clean_orders

GROUP BY order_date;

然后,通过 Hive 的INSERT OVERWRITE DIRECTORY语句将查询结果导出为 CSV 文件 :

INSERT OVERWRITE DIRECTORY '/user/hive/warehouse/reports/daily_orders'

ROW FORMAT DELIMITED

FIELDS TERMINATED BY ','

SELECT order_date, COUNT(*) AS order_count

FROM clean_orders

GROUP BY order_date;

可以使用一些工具(如 Sqoop)将 HDFS 上的报表文件导出到关系数据库(如 MySQL)中,以便在其他系统中进行展示和分析。例如,使用 Sqoop 将 Hive 报表数据导出到 MySQL 的daily_orders_report表中:

sqoop export \

--connect jdbc:mysql://your_mysql_host:3306/your_database \

--username your_username \

--password your_password \

--table daily_orders_report \

--export-dir /user/hive/warehouse/reports/daily_orders \

--fields-terminated-by ',';

7.2 日志分析案例

网站日志记录了用户在网站上的各种行为,通过对这些日志数据的分析,可以深入了解用户行为,优化网站性能和用户体验。以下以网站日志分析为例,讲解如何使用 Hive 进行日志数据处理和用户行为分析。

假设网站日志数据存储在 HDFS 上,日志格式为每行记录包含timestamp(时间戳)、user_id(用户 ID)、ip_address(IP 地址)、page_url(页面 URL)、action(用户行为,如点击、浏览、购买等)。

数据清洗:原始日志数据可能存在格式不一致、非法字符等问题,需要清洗。使用 Hive 的正则表达式函数regexp_replace清洗page_url中的非法字符。假设page_url中可能包含特殊字符$,需要将其去除:

-- 创建清洗后的日志表

CREATE TABLE clean_logs AS

SELECT timestamp,

       user_id,

       ip_address,

       regexp_replace(page_url, '\\$', '') AS page_url,

       action

FROM raw_logs;

还可以过滤掉无效的日志记录,如action字段为空的记录 :

-- 再次过滤,去除action为空的记录

CREATE TABLE final_clean_logs AS

SELECT *

FROM clean_logs

WHERE action IS NOT NULL AND action != '';

用户行为分析:利用清洗后的数据进行用户行为分析。

分析用户在一天中不同时间段的访问量

SELECT hour(from_unixtime(timestamp)) AS hour_of_day, COUNT(*) AS visit_count

FROM final_clean_logs

GROUP BY hour(from_unixtime(timestamp))

ORDER BY hour_of_day;

统计每个用户的页面浏览路径

SELECT user_id,

       collect_list(page_url) AS page_path

FROM final_clean_logs

GROUP BY user_id;

分析用户的购买转化率:假设购买行为的action值为purchase,先统计每个用户的购买次数和总访问次数 :

SELECT user_id,

       SUM(CASE WHEN action = 'purchase' THEN 1 ELSE 0 END) AS purchase_count,

       COUNT(*) AS total_visits

FROM final_clean_logs

GROUP BY user_id;

再计算购买转化率 :

SELECT user_id,

       purchase_count,

       total_visits,

       purchase_count / total_visits AS conversion_rate

FROM (

         SELECT user_id,

                SUM(CASE WHEN action = 'purchase' THEN 1 ELSE 0 END) AS purchase_count,

                COUNT(*) AS total_visits

         FROM final_clean_logs

         GROUP BY user_id

     ) subquery;

通过以上电商数据分析案例和日志分析案例可以看出,Hive 在处理大规模数据时具有强大的能力,能够帮助企业从海量数据中提取有价值的信息,为业务决策提供有力支持。无论是数据清洗、指标计算还是复杂的用户行为分析,Hive 都能通过灵活的 SQL 语句实现,展现出其在大数据分析领域的重要性和实用性。

八、总结与展望

Hive 作为大数据生态系统中的关键数据仓库工具,通过本文对其进阶知识的探索,我们深入了解了从数据定义、操作到函数应用、查询优化以及高级特性和实战案例等多方面的内容。从复杂表创建、分区与分桶优化,到高效的数据加载、灵活的数据更新与删除;从内置函数的巧用、自定义函数开发,到查询语句优化、执行计划分析;再到 ACID 与事务支持、Hive on Tez/Spark 等高级特性,以及电商和日志分析的实战应用,Hive 展现出强大的数据处理和分析能力。

这些进阶知识是提升 Hive 应用能力的关键,在实际工作中,我们应不断实践,将这些知识运用到具体项目中。持续学习和关注 Hive 的发展趋势也至关重要,随着大数据技术的不断演进,Hive 在性能优化、与其他技术的集成等方面不断发展,如 Hive 与人工智能技术的结合,将为数据处理和分析带来更多可能性。希望读者通过本文的学习,能在 Hive 的世界中不断探索,提升自己的大数据处理技能,为大数据领域的发展贡献力量。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值