MaxCompute SQL数据开发最佳实践

MaxCompute SQL最佳实践

MaxCompute SQL最佳实践是在开发文档的内容基础上,提供的一些实践性的使用指南,具体的内容见:MaxCompute SQL最佳实践文档,这里选出了一些重要且常用的内容进行说明。内容包括:

  • 快速掌握SQL写法
  • 分组取出每组数据的前N条
  • SQL实现多行数据转一条
  • MaxCompute SQL中的JOIN ON条件

快速掌握SQL写法

数据集准备

这里以emp/dept表为示例数据集。单击emp表数据文件dept表数据文件下载数据文件。这里将下载的文件存储为:csv格式文件,文件中的数据内容不变。然后在MaxCompute项目上使用如下命令创建相应的表:

创建emp表:

-- 创建emp表
CREATE TABLE IF NOT EXISTS emp (
  EMPNO string ,
  ENAME string ,
  JOB string ,
  MGR bigint ,
  HIREDATE datetime ,
  SAL double ,
  COMM double ,
  DEPTNO bigint
);

创建dept表:

-- 创建dept表
CREATE TABLE IF NOT EXISTS dept (
  DEPTNO bigint ,
  DNAME string ,
  LOC string
);

创建表完成后,上传数据使用odpscmd客户端的tunnel命令,将下载的数据文件导入到数据表中,odpscmd客户端以及MaxCompute Studio中使用odpscmd的开发环境搭建见:MaxCompute Studio数据开发工具的使用 。上传命令如下:

odps@ YITIAN_BJ_MC>tunnel upload /Users/yitian/Documents/MaxCompute/maxcompute-data/emp.csv emp;

Upload session: 202004212318540c47df0b1829ea42
Start upload:/Users/yitian/Documents/MaxCompute/maxcompute-data/emp.csv
Using \n to split records
Upload in strict schema mode: true
Total bytes:744  Split input to 1 blocks
2020-04-21 23:18:55     scan block: '1'
2020-04-21 23:18:55     scan block complete, block id: 1
2020-04-21 23:18:55     upload block: '1'
2020-04-21 23:18:55     upload block complete, block id: 1
upload complete, average speed is 744 bytes/s
OK

odps@ YITIAN_BJ_MC>tunnel upload /Users/yitian/Documents/MaxCompute/maxcompute-data/dept.csv dept;

上传完成后,可使用如下命令进行来查看导入数据的正确性:

odps@ YITIAN_BJ_MC>select count(*) from emp;
+------------+
| _c0        |
+------------+
| 14         |
+------------+

常见问题

刚开始学习MaxCompute SQL时,可能会遇到的常见问题如下:

  • 使用Group by时,Select的部分要么是分组项,要么是聚合函数。
  • Order by后面必须加Limit n。
  • Select表达式中不能用子查询,可以改写为Join。
  • Join不支持笛卡尔积,以及MapJoin的用法和使用场景。
  • Union all需要改成子查询的格式。
  • In/Not in语句对应的子查询只能有一列,而且返回的行数不能超过1000,否则也需要改成Join。

SQL示例

示例一:列出员工人数大于零的所有部门。有如下两种查询方法:

-- 列出员工人数大于0的所有部门,方法1
select d.deptno, d.dname, d.loc
from dept as d
join (
  select deptno, count(*) as num from emp
  group by deptno
) as e on d.deptno=e.deptno
where e.num>0;

-- 方法2
select d.*
from dept d
join (
  select distinct deptno as no
  from emp
) e on d.deptno=e.no;

返回的结果均为:

分析一下上面的两个方法:

就本查询来说,方法2的方式更为简单,查询效率应该更高,而且满足正确的查询需要。方法1使用了分组的思想来统计emp表中各部门的人数num,在进行join连接,并使用num作为过滤条件,这种方式的查询效率低于方法2。但如果查询需要统计:列出员工人数大于零的所有部门,并列出各部门的总人数,这种情况下方法2就不能满足需要了,但方法1中可以在第一行的输出列中加入e.num,即可以完成该查询需求。

示例二:列出薪金比SMITH多的所有员工(MapJoin使用场景)。这里依然存在两种查询方案:

-- 列出薪资比smith多的所有员工,方法1
select empno, ename, sal from emp
join (
  select max(sal) as smith_sal from emp
  where ename='SMITH'
) s on emp.sal>s.smith_sal;

-- 方法2
select /*+ mapjoin(a)*/ e.empno, e.ename, e.sal
from emp e
join (
  select max(sal) as smith_sal from emp
  where ename='SMITH'
) a on e.sal>a.smith_sal;

返回结果均为:

 

方法1为普通的join连接查询,但其实此时是MapJoin的典型场景:当一个大表和一个或多个小表JOIN时,您可以在SELECT语句中显式指定MAPJOIN以提升查询性能。这里emp相对来说是大表,而子查询为小表,因此可以使用mapjoin进行查询,以提升查询性能。

示例三:列出所有员工的姓名及其直接上级的姓名。自连接查询场景:

-- 列出所有员工的姓名及其直接上级的姓名
select t1.ename as emp_name, t2.ename as leader_name
from emp t1
left outer join emp t2 on t1.mgr=t2.empno;

返回结果如下:

示例四:列出基本薪金大于1500的所有工作(having语句使用场景)。下面有两种查询方法

-- 列出基本薪金大于1500的所有工作,方法1(错误)
select job, min(sal) as sal
from emp
where sal>1500
group by job;

-- 方法2(正确)
select job, min(sal) as sal
from emp
group by job
having min(sal)>1500;

以上的两个方法,方法1的执行顺序是:from-where-select-group by。因此它会先将sal>1500的所有job找到,然后在进行grouop by。此时由于job=SALESMAN的多条数据中,存在sal>1600的数据,因此最终查询结果会包含SALESMAN。结果如下:

但该查询的需求中“基本工资”这里文档中理解为最低工资,而SALESMAN的最低工资为1250,因此它并不应该包含在最后的结果集中,此时就需要使用方法2进行查询,方法2的查询结果为正确的结果。返回的内容如下:

示例五:列出在每个部门工作的员工数量、平均工资和平均服务期限。时间处理上有很多好用的内建函数,如下所示。

-- 列出在每个部门工作的员工数量、平均工资和平均服务期限
select deptno,
       count(empno) as cnt_emp,
       round(avg(sal)) as avg_sal,
       round(avg(datediff(getdate(), hiredate, 'dd')), 2) as avg_hire
from emp
group by deptno;

 返回结果如下:

示例六: 列出每个部门的薪水前3名的人员的姓名以及其排序(Top n的需求非常常见)。

-- 列出每个部门的薪水前3名的人员的姓名以及其排序
select *
from (
  select deptno,
         ename,
         sal,
         ROW_NUMBER() over (partition by deptno order by sal desc) as nums
  from emp
) t1
where t1.nums < 4;

返回结果如下:

 

其中使用了MaxCompute中窗口函数ROW_NUMBER,该函数由于对窗口内分组数据进行排序,并返回排序后的行号,命令格式如下,具体见:MaxCompute窗口函数文档内容。 

row_number() over(partition by [col1, col2…]
order by [col1[asc|desc], col2[asc|desc]…])

示例七: 用一个SQL写出每个部门的人数、CLERK(办事员)的人数占该部门总人数占比

-- 用一个SQL写出每个部门的人数、CLERK(办事员)的人数占该部门总人数占比
SELECT deptno, 
       COUNT(empno) AS cnt, 
       ROUND(SUM(CASE 
        WHEN job = 'CLERK' THEN 1 
        ELSE 0 
        END
       ) / COUNT(empno), 2) AS rate
FROM `EMP`
GROUP BY deptno;

返回结果如下:

 

这里对clerk人数统计时使用了MaxCompute中一些特有的函数,这部分函数包括CAST、DECODE、LEAST、ARRAY、SPLIT、MAP等,具体见:MaxCompute 其他函数文档。 

分组取出每组数据的前N条

这部分的内容其实上面已经使用过一次了(上面示例6),就是使用ROW_NUMBER窗口函数进行分组排序后,在将每组的前N条数据进行取出即可。命令如下:

SELECT * FROM (
  SELECT empno
  , ename
  , sal
  , job
  , ROW_NUMBER() OVER (PARTITION BY job ORDER BY sal) AS rn
  FROM emp
) tmp
WHERE rn < 10;

返回结果如下:

此外,可以将所有职工根据部门分组,每个组内根据sal做降序排序,获得职工自己组内的序号。

select deptno, 
       ename, 
       sal, 
       ROW_NUMBER() over (partition by deptno order by sal desc) as nums
from emp;
--执行结果如下。
+------------+-------+------------+------------+
| deptno     | ename | sal        | nums       |
+------------+-------+------------+------------+
| 10         | JACCKA | 5000.0     | 1          |
| 10         | KING  | 5000.0     | 2          |
| 10         | CLARK | 2450.0     | 3          |
| 10         | WELAN | 2450.0     | 4          |
| 10         | TEBAGE | 1300.0     | 5          |
| 10         | MILLER | 1300.0     | 6          |
| 20         | SCOTT | 3000.0     | 1          |
| 20         | FORD  | 3000.0     | 2          |
| 20         | JONES | 2975.0     | 3          |
| 20         | ADAMS | 1100.0     | 4          |
| 20         | SMITH | 800.0      | 5          |
| 30         | BLAKE | 2850.0     | 1          |
| 30         | ALLEN | 1600.0     | 2          |
| 30         | TURNER | 1500.0     | 3          |
| 30         | MARTIN | 1250.0     | 4          |
| 30         | WARD  | 1250.0     | 5          |
| 30         | JAMES | 950.0      | 6          |
+------------+-------+------------+------------+

SQL实现多行数据转一条

这部分说明如何使用 SQL 实现多条数据压缩为一条。

需求1:将emp表,根据job,获取该job中的所有ename作为一列,格式如下:

jobename
job1ename1,ename2,ename3
job2ename4,ename5

查询语句如下:

-- 将emp表,根据job,获取该job中的所有ename作为一列
select job, wm_concat(',', ename) as ename_list
from emp
group by job;

返回结果如下:

 

需求2:在emp表中,统计每个部门的总人数,CLERK职位的人数及占比,MANAGER的总人数及占比,命令如下:

-- 在emp表中,统计每个部门的总人数,CLERK职位的人数及占比,MANAGER的总人数及占比
select deptno,
       count(empno),
       sum(case when job='CLERK' then 1 else 0 end) as clerk_cnt,
       round(sum(case when job='CLERK' then 1 else 0 end)/count(empno), 2) as clerk_rate,
       sum(case when job='MANAGER' then 1 else 0 end) as manager_cnt,
       round(sum(case when job='MANAGER' then 1 else 0 end)/count(empno), 2) as manager_rate
from emp
group by deptno;

返回结果如下:

MaxCompute SQL中的JOIN ON条件

目前MaxCompute提供了以下几种Join类型:

 
类型含义
Inner Join输出符合关联条件的数据。
Left Join输出左表的所有记录,对于右表符合关联的数据,输出右表,没有符合的,右表补null。
Right Join输出右表的所有记录,对于左表符合关联的数据,输出左表,没有符合的,左表补null。
Full Join输出左表和右表的所有记录,对于没有关联上的数据,未关联的另一侧补null。
Left Semi Join对于左表中的一条数据,如果右表存在符合关联条件的行,则输出左表。
Left Anti Join对于左表中的一条数据,如果对于右表所有的行,不存在符合关联条件的数据,则输出左表。

 根据不同的场景,可以使用不同的Join类型来实现对应的关联操作。但是在实际使用过程中,存在如下错误示例。

A (LEFT/RIGHT/FULL/LEFT SEMI/LEFT ANTI) JOIN B
ON a.key = b.key and A.ds='20180101' and B.ds='20180101';

正确的join语句如下,应该是在A和B中获取某一个分区的数据,然后在进行JOIN操作:

(SELECT * FROM A WHERE ds='20180101') A
(LEFT/RIGHT/FULL/LEFT SEMI/LEFT ANTI)  JOIN
(SELECT * FROM B WHERE ds='20180101') B
ON a.key = b.key

然而针对不同的Join类型,两者可能并不等价。

连接条件和执行顺序

对于如下包含JOIN和WHERE条件的语句:

(SELECT * FROM A WHERE {subquery_where_condition} A) A
JOIN
(SELECT * FROM B WHERE {subquery_where_condition} B) B
ON {on_condition}
WHERE {where_condition}

计算顺序为:

  1. 子查询中的{subquery_where_condition}
  2. JOIN的{on_condition}的条件。
  3. JOIN结果集合{where_condition}的计算。

对于不同的JOIN类型,过滤语句放在{subquery_where_condition}{on_condition}{where_condition}中,有时结果是一致的,有时候结果又是不一致的。下面分情况进行讨论。

太长不看版

以下实验的结论总结:

过滤条件放在不同的位置语义可能大不相同,如果只是进行过滤数据后再JOIN的操作,请注意以下几点:

  • INNER JOIN/LEFT SEMI JOIN两侧的表达式可以随便写。
  • LEFT JOIN/LEFT ANTI JOIN左表的过滤条件要放到{subquery_where_condition}或者{where_condition},右表的过滤条件要放到{subquery_where_condition}或者{on_condition}中。
  • RIGHT JOIN和LEFT JOIN相反,右表的过滤条件要放到{subquery_where_condition}或者{where_condition},左表的过滤条件要放到{subquery_where_condition}或者{on_condition}。
  • FULL OUTER JOIN只能放到{subquery_where_condition}中。

如果还是觉得规则比较复杂,最好的方法就是把过滤条件写到子查询中。

详细版:各类join操作实验示例

数据准备

执行如下的建表语句:

-- JOIN操作示例数据
CREATE TABLE JOIN_A AS
  SELECT * FROM VALUES (1, 20180101),(2, 20180101),(2, 20180102) t (key, ds);

CREATE TABLE JOIN_B AS
  SELECT * FROM VALUES (1, 20180101),(3, 20180101),(2, 20180102) t (key, ds);

表A、表B的示例数据如下:

-- 表A示例数据
key	ds
+----+---+
1	20180101
2	20180101
2	20180102

-- 表B示例数据
key	ds
+----+---+
1	20180101
3	20180101
2	20180102

表A和表B的笛卡尔乘积如下:

 
a.keya.dsb.keyb.ds
120180101120180101
120180101320180101
120180101220180102
220180101120180101
220180101320180101
220180101220180102
220180102120180101
220180102320180101
220180102220180102

inner join测试 

Inner Join的处理逻辑是将左右表进行笛卡尔乘积,然后选择满足ON表达式的行进行输出。

1. 第一种情况,在子查询中过滤:

-- inner join 情况1
select a.*, b.*
from (
  select * from JOIN_A where ds='20180101'
) a join (
  select * from JOIN_B where ds='20180101'
) b on a.key=b.key;

结果如下:

 
a.keya.dsb.keyb.ds
120180101120180101

2. 第二种情况,在JOIN 条件中过滤:

-- inner join 情况2
select a.*, b.*
from JOIN_A a
join JOIN_B b on a.key=b.key and a.ds='20180101' and b.ds='20180101';

笛卡尔积的结果有9条,满足ON条件的结果同样只有1条。

 
a.keya.dsb.keyb.ds
120180101120180101

3. 第三种情况,在JOIN后的WHERE条件过滤:

-- inner join 情况3
select a.*, b.*
from JOIN_A a
join JOIN_B b on a.key=b.key
where a.ds='20180101' and b.ds='20180101';

笛卡尔积的结果有9条,满足ON条件a.key = b.key的结果有3条,如下。

 
a.keya.dsb.keyb.ds
120180101120180101
220180102220180102
220180101220180102

此时对于这个结果再进行过滤A.ds='20180101' and B.ds='20180101',结果只有1条。

 
a.keya.dsb.keyb.ds
120180101120180101

可以看到,将过滤条件放在三个不同的地方,得到了相同的结果。仅此该部分的结论如下:

结论:inner join中,过滤条件在{subquery_where_condition}{on_condition}{where_condition}中都是等价的。

Left Join

Left Join的处理逻辑是将左右表进行笛卡尔乘积,然后对于满足ON表达式的行进行输出,对于左表中不满足ON表达式的行,输出左表,右表补NULL。

1. 第一种情况,在子查询中过滤:

-- left join 情况1
select a.*, b.*
from (
  select * from JOIN_A where ds='20180101'
) a left join (
  select * from JOIN_B where ds='20180101'
) b on a.key=b.key;

过滤后,左右侧有两条,右侧有一条,结果有两条:

 
a.keya.dsb.keyb.ds
120180101120180101
220180101NULLNULL

并且,left join等价于left outer join,上面命令等价于如下,查询结果相同:

select a.*, b.*
from (
  select * from JOIN_A where ds='20180101'
) a left outer join (
  select * from JOIN_B where ds='20180101'
) b on a.key=b.key;

 2. 第二种情况,在JOIN 条件中过滤:

select a.*, b.*
from JOIN_A a
left join JOIN_B b on a.key=b.key and a.ds='20180101' and b.ds='20180101';

笛卡尔积的结果有9条,满足ON条件的结果同样只有1条,则对于左表剩余的两条输出左表,右表补NULL。

 
a.keya.dsb.keyb.ds
120180101120180101
220180101NULLNULL
220180102NULLNULL

3. 第三种情况,JOIN后的WHERE条件过滤:

-- left join 情况3
select a.*, b.*
from JOIN_A a
left join JOIN_B b on a.key=b.key
where a.ds='20180101' and b.ds='20180101';

笛卡尔积的结果有9条,满足ON条件a.key = b.key的结果有3条。

 
a.keya.dsb.keyb.ds
120180101120180101
220180101220180102
220180102220180102

此时对于这个结果再进行过滤A.ds='20180101' and B.ds='20180101',结果只有1条。

 
a.keya.dsb.keyb.ds
120180101120180101

可以看到,将过滤条件放在三个不同的地方,得到了三种不同的结果。

结论:过滤条件在{subquery_where_condition}{on_condition}{where_condition}不一定等价。

  • 对于左表的过滤条件,放在{subquery_where_condition}{where_condition}是等价的。
  • 对于右表的过滤条件,放在{subquery_where_condition}{on_condition}中是等价的。

Right Join

Right Join和Left Join是类似的,只是左右表的区别,结论如下:

  • 过滤条件在{subquery_where_condition}{on_condition}{where_condition}不一定等价。
  • 对于右表的过滤条件,放在{subquery_where_condition}{where_condition}是等价的。
  • 对于左表的过滤条件,放在{subquery_where_condition}{on_condition}中是等价的。

Full Join

FULL Join的处理逻辑是将左右表进行笛卡尔乘积,然后对于满足ON表达式的行进行输出,对于两侧表中不满足ON表达式的行,输出有数据的表,另一侧补NULL。

1. 第一种情况,子查询中过滤:

-- full join 情况1
select a.*, b.*
from (
  select * from JOIN_A where ds='20180101'
) a full join (
  select * from JOIN_B where ds='20180101'
) b on a.key=b.key;

过滤后,左右侧有两条,右侧有两条,结果有三条。

 
a.keya.dsb.keyb.ds
120180101120180101
220180101NULLNULL
NULLNULL320180101

2. 第二种情况,JOIN 条件中过滤:

-- full join 情况2
select a.*, b.*
from JOIN_A a
full join JOIN_B b on a.key=b.key and a.ds='20180101' and b.ds='20180101';

笛卡尔积的结果有9条,满足ON条件的结果同样只有1条,则对于左表剩余的两条输出左表,右表补NULL。右表剩余的两条输出右表,左表补NULL。

 
a.keya.dsb.keyb.ds
120180101120180101
220180101NULLNULL
220180102NULLNULL
NULLNULL320180101
NULLNULL220180102

3. 第三种情况,JOIN后的WHERE条件过滤:

-- full join 情况3
select a.*, b.*
from JOIN_A a
full join JOIN_B b on a.key=b.key
where a.ds='20180101' and b.ds='20180101';

笛卡尔积的结果有9条,满足ON条件a.key = b.key的结果有3条。

 
a.keya.dsb.keyb.ds
120180101120180101
220180101220180102
220180102220180102

再对没有JOIN上的数据进行输出,另一侧补NULL,得到结果如下。

 
a.keya.dsb.keyb.ds
120180101120180101
220180101220180102
220180102220180102
NULLNULL320180101

此时对于这个结果再进行过滤A.ds='20180101' and B.ds='20180101',结果只有1条。

 
a.keya.dsb.keyb.ds
120180101120180101

可以看到,和LEFT JOIN类似,得到了三种不同的结果。

结论:过滤条件写在{subquery_where_condition}{on_condition}{where_condition}均不等价。

Left Semi Join

LEFT SEMI Join的处理逻辑是对于左表的每一条记录,都去和右表进行匹配,如果匹配成功,则输出左表。这里需要注意的是由于只输出左表,所以JOIN后的Where条件中不能写右侧的过滤条件。LEFT SEMI JOIN常用来实现exists的语义

1. 第一种情况,在子查询中过滤:

-- left semi join 情况1
select a.*
from (
  select * from JOIN_A where ds='20180101'
) a left semi join (
  select * from JOIN_B where ds='20180101'
) b on a.key=b.key;

过滤后,左右侧有两条,最终符合a.key = b.key的只有一条。

 
a.keya.ds
120180101

2. 第二种情况,JOIN 条件中过滤:

-- left semi join 情况2
select a.*
from JOIN_A a
left semi join JOIN_B b on a.key=b.key and a.ds='20180101' and b.ds='20180101';

对于左侧的三条记录,满足ON条件的结果同样只有1条。

 
a.keya.ds
120180101

3. 第三种情况,JOIN后的WHERE条件过滤:

-- left semi join 情况3
select a.*
from JOIN_A a
left semi join (
  select * from JOIN_B where ds='20180101'
) b on a.key=b.key
where a.ds='20180101';

左侧能符合ON条件的有一条。

 
a.keya.ds
120180101

此时对于这个结果再进行过滤A.ds='20180101',结果仍然保持1条。

 
a.keya.ds
120180101

可以看到,LEFT SEMI JOIN和INNER JOIN类似,无论过滤条件放在哪里,结果都是一致的。

结论:过滤条件写在{subquery_where_condition}{on_condition}{where_condition}是等价的。

Left Anti Join

LEFT ANTI Join的处理逻辑是对于左表的每一条记录,都去和右表进行匹配,如果右表所有的记录都没有匹配成功,则输出左表。同样由于只输出左表,所以JOIN后的Where条件中不能写右侧的过滤条件。LEFT SEMI JOIN常常用来实现not exists的语义。

1. 第一种情况,子查询中过:

-- left anti join 情况1
select a.*
from (
  select * from JOIN_A where ds='20180101'
) a left anti join (
  select * from JOIN_B where ds='20180101'
) b on a.key=b.key;

过滤后,左侧有两条,右侧有两条,结果有1条。

 
a.keya.ds
220180101

2. 第二种情况,JOIN 条件中过滤:

-- left anti join 情况2
select a.*
from JOIN_A a
  left anti join JOIN_B b on a.key=b.key and a.ds='20180101' and b.ds='20180101';

对于左侧的三条记录,只有第一条有满足ON条件的结果,所以输出剩余的两条记录。

 
a.keya.ds
220180101
220180102

3. 第三种情况,JOIN后的WHERE条件过滤:

-- left anti join 情况3
select a.*
from JOIN_A a
  left anti join (
  select * from JOIN_B where ds='20180101'
  ) b on a.key=b.key
where a.ds='20180101';

左侧能通过ON条件的有两条。

 
a.keya.ds
220180101
220180102

此时对于这个结果再进行过滤A.ds='20180101',结果为1条。

 
a.keya.ds
220180101

可以看到,LEFT ANTI JOIN中,过滤条件WHERE语句分别放在JOIN ON条件中、条件前和条件后,得到的结果是不相同的。

结论:过滤条件写在{subquery_where_condition}{on_condition}{where_condition}不一定等价。

  • 对于左表的过滤条件,放在{subquery_where_condition}{where_condition}是等价的。
  • 对于右表的过滤条件,放在{subquery_where_condition}{on_condition}中是等价的,右表表达式不能放在{where_condition}中。

以上内容只是针一个常用场景测试的几种不同的写法,没有具体的推导过程,对于涉及到不等值表达式的场景会更加复杂,如果您有兴趣可以尝试推导一下。

 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值