深入浅出Hive性能优化策略

        我们将从基础的HiveQL优化讲起,涵盖数据存储格式选择、数据模型设计、查询执行计划优化等多个方面。会的直接滑到最后看代码和语法。

目录

引言

Hive架构概览

示例1:创建表并加载数据

示例2:优化查询

Hive查询优化

1. 选择适当的文件格式

2. 利用分区和分桶

3. 使用合适的JOIN策略

4. 优化HiveQL语句

Hive参数调优

1. hive.exec.parallel

2.hive.exec.parallel.thread.number

3.hive.exec.dynamic.partition

4.hive.vectorized.execution.enabled

5.mapreduce.job.reduces

6.hive.optimize.sort.dynamic.partition

实践建议

技巧总结


引言

        在当今这个数据驱动的时代,数据已成为企业制胜的关键。众多企业和组织正通过海量数据的分析和处理来挖掘有价值的信息,以支持决策制定,优化业务流程,提升客户体验,甚至开发新的商业模式。在这一背景下,Apache Hive作为一个建立在Hadoop生态系统之上的数据仓库工具,因其能够提供类SQL查询功能而变得极为重要。Hive使得即使是不熟悉Java或MapReduce的数据分析师也能轻松处理大规模数据集。

        Hive的设计初衷是用于数据汇总、查询和分析,但随着数据量的日益增长,性能优化成为了使用Hive时不可或缺的一部分。无论是在数据查询、数据存储格式,还是在执行策略上,Hive都提供了多种优化手段,以满足不同场景下对性能的需求。

        性能优化不仅可以减少资源的浪费,提高查询的响应速度,还能在一定程度上降低计算成本,提升用户体验。优化的过程就像是在寻找最佳路径一样,需要对Hive的内部机制有深入的了解,同时也需要根据实际情况灵活应变,才能找到最适合自己业务场景的优化方案。

        在探索Hive优化策略中,我们将从基础的HiveQL优化讲起,涵盖数据存储格式选择、数据模型设计、查询执行计划优化等多个方面。我们也会讨论如何通过调整Hive配置和使用资源管理器来优化资源利用率,以及如何根据实际的业务需求和数据特性来选择合适的优化手段。

Hive架构概览

        Apache Hive 是一个构建在 Hadoop 生态系统之上的数据仓库软件,用于数据提取、转换和加载(ETL)任务。它提供了一种类似 SQL 的查询语言,称为 HiveQL,让那些熟悉 SQL 的用户可以轻松地进行数据查询和分析。为了更好地理解 Hive 如何进行性能优化,我们首先需要对其架构有一个基本的了解。

Hive 的架构主要包括以下几个组件:

  1. 用户接口:Hive 支持多种用户接口,包括命令行工具(Hive CLI)、Web界面和 JDBC/ODBC 驱动程序。
  2. Hive Server:它允许客户端使用 Thrift 协议远程提交请求到 Hive。
  3. 元数据存储:Hive 使用关系型数据库(如 MySQL、PostgreSQL)存储元数据,包括表的定义、列数据类型、分区信息等。
  4. 执行引擎:Hive 查询最初是通过 MapReduce 执行的,但现在它也支持 Tez 和 Spark 等其他执行引擎,以提高性能。
  5. HDFS:Hive 存储其数据在 Hadoop 分布式文件系统(HDFS)中,利用 HDFS 的高可靠性和高吞吐量。

示例1:创建表并加载数据

为了展示 Hive 的基本用法,我们首先通过一个简单的示例来创建一个 Hive 表,并向其中加载一些数据。

CREATE TABLE IF NOT EXISTS employees (
  id INT,
  name STRING,
  age INT,
  department STRING
)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY ','
STORED AS TEXTFILE;

这段代码创建了一个名为 employees 的表,其中包含 idnameagedepartment 四个字段。字段之间通过逗号分隔。

接下来,我们将数据加载到这个表中。

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

此命令将本地文件系统中的 employees.txt 文件中的数据加载到 employees 表中。假设该文本文件的每一行都是一个记录,字段之间由逗号分隔。

示例2:优化查询

理解了 Hive 的基础架构后,我们可以通过一些优化技巧来提高查询的性能。假设我们想要查询 department 为 'Sales' 的所有员工,一个未优化的查询可能如下所示:

SELECT * FROM employees WHERE department = 'Sales';

为了优化这个查询,我们可以考虑使用分区。首先,重新创建 employees 表,并按 department 进行分区:

CREATE TABLE employees_partitioned (
  id INT,
  name STRING,
  age INT
)
PARTITIONED BY (department STRING)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY ','
STORED AS TEXTFILE;

然后,我们可以针对特定的 department 分区执行查询,这样 Hive 只需扫描相关的分区数据,而不是整个表:

SELECT * FROM employees_partitioned WHERE department = 'Sales';

通过这种方式,我们可以显著减少查询所需扫描的数据量,从而提高查询效率。

Hive查询优化

        在大数据处理中,编写高效的查询是提高数据处理速度的关键之一。Hive提供了多种方式来优化查询,从而减少执行时间和资源消耗。以下是一些常用的查询优化技巧:

1. 选择适当的文件格式

Hive支持多种文件格式,包括文本文件、SequenceFile、ORC、Parquet等。选择合适的文件格式对于查询性能有显著影响。例如,ORC(Optimized Row Columnar)格式提供了高效的压缩和编码方案,能够显著减少存储空间并加速查询。

示例:

假设我们有一个大型数据集需要频繁查询,我们可以选择ORC格式来存储数据:

CREATE TABLE employees_orc (
  id INT,
  name STRING,
  age INT,
  department STRING
)
STORED AS ORC;

使用ORC格式后,查询同样的数据将更快,因为ORC格式提供了更好的读取性能。

2. 利用分区和分桶

通过将数据分区和分桶,Hive能够更快地定位到查询所需的数据子集,从而减少查询所需扫描的数据量。

示例:

假设我们想要根据部门对员工数据进行分区,并在每个部门内部根据年龄进行分桶:

CREATE TABLE employees_partitioned_bucketed (
  id INT,
  name STRING,
  age INT
)
PARTITIONED BY (department STRING)
CLUSTERED BY (age) INTO 10 BUCKETS
STORED AS ORC;

在这个表中,数据首先按部门进行分区,然后每个部门内的数据根据员工年龄分成10个桶。这样,当执行涉及特定部门和年龄范围的查询时,Hive只需扫描相关的分区和桶,大大提升查询效率。

3. 使用合适的JOIN策略

Hive支持多种JOIN策略,包括MapJoin、SortMergeJoin等。在某些情况下,明确指定JOIN策略可以优化查询性能。

示例:

当我们知道参与JOIN的一个表非常小的时候,可以使用MapJoin来加速处理:

SET hive.auto.convert.join=true;
SET hive.auto.convert.join.noconditionaltask.size=100000;

SELECT /*+ MAPJOIN(small_table) */ *
FROM big_table
JOIN small_table ON big_table.id = small_table.id;

在这个示例中,我们假设small_table的大小足够小,可以完全装载进内存,通过提示Hive使用MapJoin,可以在内存中直接进行JOIN操作,从而加快查询速度。

4. 优化HiveQL语句

编写高效的HiveQL语句也是优化查询的一个重要方面。例如,避免使用SELECT *,而是只选择需要的列,可以减少数据传输和处理的开销。

示例:

-- 不推荐的写法
SELECT * FROM employees WHERE department = 'Sales';

-- 推荐的写法
SELECT id, name FROM employees WHERE department = 'Sales';

在推荐的写法中,我们只选择了idname列,而不是选择所有列,这样可以减少数据的读取和传输量,提高查询效率。

Hive参数调优

Hive的性能不仅取决于查询的写法或数据的存储方式,还受到Hive配置参数的极大影响。正确调整这些参数可以显著提高查询速度和处理效率。下面,我们将探讨一些关键的Hive性能调优参数。

1. hive.exec.parallel

这个参数默认为false,意味着Hive在执行任务时不会并行处理。如果将其设置为true,Hive会尝试并行执行多个任务,这可以显著减少执行时间。

SET hive.exec.parallel = true;

2.hive.exec.parallel.thread.number

当启用并行执行时,此参数控制并行执行的线程数。调整此参数以适应你的集群资源和任务负载。

SET hive.exec.parallel.thread.number = 8;

3.hive.exec.dynamic.partition

此参数用于控制Hive是否启用动态分区。启用动态分区(设置为true)可以在执行插入操作时自动创建分区,这对于处理大量分区非常有用。

SET hive.exec.dynamic.partition = true; 
SET hive.exec.dynamic.partition.mode = nonstrict;

4.hive.vectorized.execution.enabled

启用向量化查询执行可以显著提高查询性能,因为它使得Hive在处理数据批次时能够利用CPU的向量化指令。默认情况下,这个选项可能是关闭的。

SET hive.vectorized.execution.enabled = true; 
SET hive.vectorized.execution.reduce.enabled = true;

5.mapreduce.job.reduces

虽然这是一个MapReduce级别的参数,但它也影响Hive的性能。此参数控制Reduce任务的数量。合理设置此值可以平衡负载并减少执行时间。

SET mapreduce.job.reduces = 10;

6.hive.optimize.sort.dynamic.partition

当设置为true时,此参数会对动态分区操作进行排序,以减少作为Reduce阶段一部分的I/O操作。这对于提高包含大量动态分区的查询的性能非常有用。

SET hive.optimize.sort.dynamic.partition = true;

实践建议

        在调整这些参数时,重要的是要记住,并没有一套适合所有情况的最佳设置。最佳的参数设置取决于具体的查询类型、数据量、集群大小和其他因素。因此,进行参数调优时应该采取迭代的方法,逐一调整参数,观察性能变化,从而找到最适合你当前工作负载的配置。

技巧总结

        各种优化技巧和相应代码示例。这些优化措施包括但不限于并行处理、动态分区、向量化查询执行以及MapReduce作业的调整。

-- 启用并行执行以提高任务处理速度
SET hive.exec.parallel = true;
SET hive.exec.parallel.thread.number = 8; -- 根据你的集群资源调整线程数

-- 启用动态分区以便在执行插入操作时自动创建分区
SET hive.exec.dynamic.partition = true;
SET hive.exec.dynamic.partition.mode = nonstrict;

-- 启用向量化查询执行,以利用CPU的向量化指令来加速处理
SET hive.vectorized.execution.enabled = true;
SET hive.vectorized.execution.reduce.enabled = true;

-- 调整Reduce任务的数量以平衡负载并减少执行时间
SET mapreduce.job.reduces = 10; -- 根据数据量和查询复杂度来调整

-- 对动态分区操作进行排序,以减少Reduce阶段的I/O操作
SET hive.optimize.sort.dynamic.partition = true;

-- 示例:创建分区表并使用优化的查询
CREATE TABLE employees_partitioned (
  id INT,
  name STRING,
  age INT
)
PARTITIONED BY (department STRING)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY ','
STORED AS TEXTFILE;

-- 加载数据进入分区表
LOAD DATA LOCAL INPATH '/path/to/employees.txt' INTO TABLE employees_partitioned PARTITION(department);

-- 针对特定分区执行查询,减少扫描数据量
SELECT * FROM employees_partitioned WHERE department = 'Sales';

        一些查询优化的实用示例,比如使用合适的JOIN类型、合理利用WHERE子句来过滤数据,以及使用合适的数据存储格式和分区策略来提高查询效率

-- 启用向量化查询执行
SET hive.vectorized.execution.enabled = true;
SET hive.vectorized.execution.reduce.enabled = true;

-- 限制查询结果,仅用于测试和开发阶段
SELECT name, age FROM employees WHERE age > 30 LIMIT 100;

-- 使用INNER JOIN代替CROSS JOIN,并在JOIN之前过滤数据
SELECT e.name, d.department_name
FROM employees e
JOIN departments d ON e.department_id = d.id
WHERE e.age > 25 AND d.location = 'New York';

-- 使用MAPJOIN优化小表JOIN大表
SELECT /*+ MAPJOIN(small_table) */ big_table.*
FROM big_table
JOIN small_table ON big_table.key = small_table.key;

-- 使用窗口函数进行优化的聚合查询
SELECT department, AVG(salary) OVER (PARTITION BY department) as avg_salary
FROM employees;

-- 使用SORT BY进行局部排序,避免全局排序的开销
SELECT * FROM employees ORDER BY name SORT BY age;

-- 使用分区键进行查询,减少扫描的数据量
SELECT * FROM employees_partitioned WHERE department = 'Sales';

-- 使用DISTRIBUTE BY和SORT BY组合优化GROUP BY操作
SELECT department, COUNT(*) FROM employees
DISTRIBUTE BY department
SORT BY department
GROUP BY department;

-- 使用EXPLAIN命令检查执行计划
EXPLAIN
SELECT name, age FROM employees WHERE age > 30;

-- 使用COLLECT_SET来去重聚合
SELECT department, COLLECT_SET(name)
FROM employees
GROUP BY department;

-- 避免使用NOT IN和NOT EXISTS,使用LEFT SEMI JOIN代替
SELECT e.name
FROM employees e
LEFT SEMI JOIN departments d ON e.department_id = d.id
WHERE d.department_name = 'Sales';

-- 注意:每一种优化策略都需要根据具体的查询和数据环境进行调整和测试以验证其有效性。

具体的HiveQL代码示例

-- 1. 使用内连接代替全连接,减少数据量
SELECT a.*, b.*
FROM table_a a
JOIN table_b b ON a.key = b.key;

-- 2. 在JOIN前使用WHERE子句过滤,减少JOIN操作的数据量
SELECT a.*, b.*
FROM table_a a
JOIN table_b b ON a.key = b.key
WHERE a.date = '2024-03-17';

-- 3. 利用MAPJOIN优化小表与大表的JOIN操作
SELECT /*+ MAPJOIN(small_table) */ big_table.*, small_table.*
FROM big_table
JOIN small_table ON big_table.key = small_table.key;

-- 4. 仅选择需要的列,避免使用SELECT *
SELECT id, name, department
FROM employees;

-- 5. 使用分区查询,减少扫描的数据量
SELECT *
FROM sales_data
WHERE partition_date = '2024-03-17';

-- 6. 使用SORT BY代替ORDER BY进行局部排序
SELECT name, age
FROM employees
SORT BY age;

-- 7. 使用CLUSTER BY在分布式处理时同时进行数据分配和排序
SELECT name, department
FROM employees
CLUSTER BY department;

-- 8. 使用LIMIT进行测试,限制结果集大小
SELECT *
FROM large_table
LIMIT 100;

-- 9. 使用EXPLAIN命令分析查询执行计划
EXPLAIN
SELECT name, sum(salary)
FROM employees
GROUP BY name;

-- 10. 开启向量化查询执行
SET hive.vectorized.execution.enabled = true;
SET hive.vectorized.execution.reduce.enabled = true;

-- 11. 压缩MapReduce作业的中间结果
SET hive.exec.compress.intermediate = true;

-- 12. 使用窗口函数优化聚合操作
SELECT name,
       department,
       AVG(salary) OVER (PARTITION BY department) as avg_dept_salary
FROM employees;

-- 13. 使用COLLECT_SET聚合函数去重
SELECT department, COLLECT_SET(name)
FROM employees
GROUP BY department;

-- 14. 使用DISTRIBUTE BY和SORT BY优化GROUP BY操作,减少数据倾斜
SELECT department, count(*)
FROM employees
DISTRIBUTE BY department
SORT BY department;

-- 15. 使用SEMI JOIN减少数据传输
SELECT a.*
FROM table_a a
WHERE EXISTS (SELECT 1 FROM table_b b WHERE a.key = b.key);

-- 16. 避免复杂正则表达式,简化查询条件
SELECT *
FROM logs
WHERE url LIKE '%openai%';

-- 17. 优化CASE语句,将最可能的情况放在前面
SELECT name,
       CASE WHEN age < 20 THEN 'Generation Z'
            WHEN age BETWEEN 20 AND 39 THEN 'Millennials'
            ELSE 'Other'
       END as generation
FROM employees;

-- 18. 使用动态分区插入,优化数据写入操作
SET hive.exec.dynamic.partition = true;
SET hive.exec.dynamic.partition.mode = nonstrict;
INSERT INTO TABLE employees_partitioned PARTITION(department)
SELECT id, name, age, department
FROM employees_staging;

-- 19. 使用TEZ引擎优化执行
SET hive.execution.engine=tez;

-- 20. 优化GROUP BY操作,使用GROUP BY ... SKEWED BY
SET hive.groupby.skewindata=true;
SELECT department, count(*)
FROM employees
GROUP BY department;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/472321.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

安卓findViewById 的优化方案:ViewBinding与ButterKnife(一)

好多小伙伴现在还用findViewById来获取控件的id, 在这里提供俩种替代方案&#xff1a;ViewBinding与ButterKnife&#xff1b; 先来说说ButterKnife ButterKnife ButterKnife是一个专注于Android系统的View注入框架&#xff0c;在过去的项目中总是需要很多的findViewById来查…

Java Day13 多线程

多线程 1、 方式一 Thread2、实现Runnable接口3、实现 Callable接口4、与线程有关的操作方法5、线程安全问题5.1 取钱案例5.2 线程同步5.2.1 同步代码块5.2.2 同步方法5.2.3 Lock锁 6、线程池6.2 创建线程池6.2.1 使用ExecutorService创建新任务策略6.2.2 使用Executors工具类创…

2024年云仓酒庄佛山发布会:赋能

原标题&#xff1a;2024年云仓酒庄佛山发布会圆满落幕&#xff0c;朱囿臻总赋能引领行业新篇章 近日&#xff0c;备受瞩目的云仓酒庄佛山发布会圆满落幕。此次发布会汇聚了业内精英、经销商代表以及媒体人士&#xff0c;共同见证了云仓酒庄在佛山市场的启航。在此&#xff0c;…

智慧公厕:卫生、便捷、安全的新时代厕所变革

在城市快速发展的背景下&#xff0c;公共厕所的建设和管理变得越来越重要。智慧公厕作为厕所变革的一项全新举措&#xff0c;通过建立公共厕所全面感知监测系统&#xff0c;以物联网、互联网、大数据、云计算、自动化控制技术为支撑&#xff0c;实现对公共厕所的智能化管理和运…

练习4-权重衰减(李沐函数简要解析)

环境:练习1的环境 代码详解 0.导入库 import torch from torch import nn from d2l import torch as d2l1.初始化数据 这里初始化出train_iter test_iter 可以查一下之前的获取Fashion数据集后的数据格式与此对应 n_train, n_test, num_inputs, batch_size 20, 100, 200, …

50. 【Linux教程】源码安装软件

本小节介绍如何使用软件的源码包安装软件&#xff0c;以安装 nginx 源码包为例。 1.下载软件源码包 使用如下命令下载 nginx 源码包&#xff1a; wget http://nginx.org/download/nginx-1.18.0.tar.gz执行结果如下图所示&#xff1a; 2.解压源码包 下载好了压缩包之后&#…

基于Java+SpringBoot+vue+element实现前后端分离玩具商城系统

基于JavaSpringBootvueelement实现前后端分离玩具商城系统 博主介绍&#xff1a;多年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 央顺技术团队 Java毕设项目精品实战案例《1000套》 欢迎点赞 收藏 ⭐留言 文…

Linux网络编程: TCP协议之序号和确认号详解

一、TCP协议首部 二、序号&#xff08;Sequence Number&#xff09; 32位&#xff0c;表示该报文段所发送数据的第一个字节的编号。 实际上 TCP 的序号并不是按照 “一条两条” 这样的方式来编号的。在TCP连接中所传输字节流的每一个字节都会按顺序编号&#xff0c;由于序列号…

CTF-reverse-每日练题-xxxorrr

题目链接 https://adworld.xctf.org.cn/challenges/list 题目详情 xxxorrr ​ 解题报告 下载得到的文件使用ida64分析&#xff0c;如果报错就换ida32&#xff0c;得到分析结果&#xff0c;有main函数就先看main main函数分析 v6 main函数中&#xff0c;v6的值是__readfsqwor…

Java基础学习笔记三

环境变量CLASSPATH classpath环境变量是隶属于java语言的&#xff0c;不是windows操作系统的&#xff0c;和PATH环境变量完全不同classpath环境变量是给classloader&#xff08;类加载器&#xff09;指路的java A 。执行后&#xff0c;先启动JVM&#xff0c; JVM启动classload…

聚类算法( clustering algorithm):

在前两章&#xff0c;我们学的是&#xff1a;线性回归&#xff0c;逻辑回归&#xff0c;深度学习(神经网络)&#xff0c;决策树&#xff0c;随即森林算法。他们都是监督学习的例子。 在这一章里&#xff0c;我们将学习非监督学习的算法。 什么是聚类算法&#xff1a; 聚类算…

C语言结构体详解

1、结构体的声明 结构体是一些值的集合&#xff0c;这些值被称为成员变量。结构体中的每个成员可以是不同类型的变量。 语法&#xff1a; struct tag //关键词 标签 { member- list ;//成员清单 }variable- list ;//变量清单 通常结构体描述的是一个复杂对象&#xff0c;比…

【Linux】多线程概念 | POSIX线程库

文章目录 一、线程的概念1. 什么是线程Linux下并不存在真正的多线程&#xff0c;而是用进程模拟的&#xff01;Linux没有真正意义上的线程相关的系统调用&#xff01;原生线程库pthread 2. 线程和进程的联系和区别3. 线程的优点4. 线程的缺点5. 线程异常6. 线程用途 二、二级页…

2023 re:Invent | Amazon Q 与 Amazon CodeWhisperer 面向企业开发者提效利器

2023 年&#xff0c;以 GPT 为代表的生成式 AI 引爆了新一轮技术热潮&#xff0c;短短一年的时间内&#xff0c;生成式 AI 已经成为科技世界发展的核心。作为云计算的行业风向标盛会 re &#xff0c;本届: Invent 全球大会紧跟生成式 AI 浪潮&#xff0c;推出名为“ Amazon Q ”…

【方法】想要修改PDF文件怎么办?

在工作上&#xff0c;我们经常需要用到PDF文件&#xff0c;但需要修改PDF时&#xff0c;有些小伙伴却不知道怎么办&#xff0c;那就一起来看看以下两个方法吧&#xff01; 方法一&#xff1a;使用PDF编辑器 PDF文件可以通过PDF阅读器或者浏览器在线打开&#xff0c;但无法进行…

【DFS】第十三届蓝桥杯省赛C++ B组《扫雷》(C++)

【题目描述】 小明最近迷上了一款名为《扫雷》的游戏。 其中有一个关卡的任务如下&#xff1a; 在一个二维平面上放置着 n 个炸雷&#xff0c;第 i 个炸雷 (xi,yi,ri) 表示在坐标 (xi,yi) 处存在一个炸雷&#xff0c;它的爆炸范围是以半径为 ri 的一个圆。 为了顺利通过这片…

GIS设计与开发的学习笔记

目录 一、简答题 1.GeoDatabase数据模型结构类型与四种关系。 2.组件式GIS的基本思想是什么&#xff1f; 3.请简述创建空间书签的实现逻辑。 4.请问与地理要素编辑相关的类有哪些&#xff1f;&#xff08;列举至少五个类&#xff09; 5.利用ArcGIS Engine提供的栅格运算工…

电玩体验店怎么计时,佳易王ps5计时计费管理控制系统操作教程

电玩体验店怎么计时&#xff0c;佳易王ps5计时计费管理控制系统操作教程 一、前言 以下软件操作教程以 佳易王电玩计时计费管理系统软件V17.9为例说明 件文件下载可以点击最下方官网卡片——软件下载——试用版软件下载 1、电玩体验馆管理软件在计时的同时可以设置定时提醒&…

大模型第一讲笔记

目录 1、人工智能基础概念全景介绍... 2 1.1 人工智能全景图... 2 1.2 人工智能历史... 2 1.3 人工智能——机器学习... 3 监督学习、非监督学习、强化学习、机器学习之间的关系... 3 监督学习... 4 无监督学习... 5 强化学习... 5 深度学习... 6 2、语言模型的发展及…

视频素材库app推荐的地方在哪里找?

视频素材库app推荐的地方在哪里&#xff1f;这是很多短视频创作者都会遇到的问题。别着急&#xff0c;今天我就来给大家介绍几个视频素材库app推荐的网站&#xff0c;让你的视频创作更加轻松有趣&#xff01; 蛙学网&#xff1a;视频素材库app推荐的首选当然是蛙学网啦&#xf…
最新文章