介绍:如何充分利用 SQL 是本文的主题。 本文试图找到一种独特的方法,强调通过灵活和发散的数据处理思维,可以用最基本的语法解决复杂的数据场景。
一、引言
一、初衷
如何高效使用MaxCompute(ODPS)SQL,并充分利用SQL基本语法。
在如此火爆的大数据的今天,不仅专业的数据人员,还需要经常与SQL打交道,即使是非技术类的学员,如产品和运营人员都会或多或少地使用SQL,如何高效发挥SQL的能力,进而充分发挥数据的能力,就变得尤为重要。
MaxCompute(ODPS)SQL作为SQL方言,具有完整的SQL语法支持、丰富的内置函数,以及开窗函数、用户自定义函数、用户自定义类型等诸多高级特性,可以高效应用于各种数据处理场景。
如何充分利用 SQL 是本文的主题。 本文试图找到一种新方法它强调通过灵活和发散的数据处理思维,可以用最基本的语法解决复杂的数据场景。
2.适合人群
本文可能对初学者和老手有所帮助,但更适合中级和高级读者。
同时,为避免话题分歧,文章中涉及的功能和语法特点不会以特殊方式介绍,读者可以自行理解。
3. 内容结构
在本文中,我们将介绍序列生成、区间变换、排列组合和连续判别等主题,并通过实例说明其实际应用。 每个主题之间有轻微的依赖关系,最好依次阅读。
4. 提示信息
本文涉及的SQL语句仅使用MaxCompute(ODPS)SQL的基本语法特性,理论上所有SQL语句都可以在当前最新版本下运行。
二、数字序列
序列是最常见的数据形式之一,大多数是实际数据开发场景中遇到的有限序列。 本节将从最简单的递增序列开始,找出一般方法,并推广到更通用的方案。
1.常用号码系列
1)简单的升序数字序列
首先,引入一个简单的整数递增序列:
从值 0 开始;
每个后续值递增 1;
到值 3 的末尾;
如何生成满足上述三个条件的系列? 即 [0,1,2,3]。
事实上,有几种方法可以生成这个序列,这里有一个简单而通用的方法。
sql - 1
select
t.pos as a_n
from (
select posexplode(split(space(3), space(1), false))
t;
从上面的 SQL 代码片段中可以看出,生成递增序列只需三个步骤:
生成一个适当长度的数组,数组中的元素不需要有实际意义;
通过 UDTF 函数 posexplode 为数组中的每个元素生成一个索引下标;
取出每个元素的索引下标。 以上三个步骤可以推广到更一般的序列场景:等差级数、等比例级数。 基于此,最终的实现模板将直接在下面给出。
2)等差级数
如果设置了第一项。 公差为:
那么差分级数的一般公式是。
SQL 实现:
sql - 2
select
a + t.pos * d as a_n
from (
select posexplode(split(space(n - 1), space(1), false))
t;3) 比例系列
如果设置了第一项。 常见的比例是。
那么比例级数的一般公式是。
SQL 实现:
sql - 3
select
a * pow(q, t.pos) as a_n
from (
select posexplode(split(space(n - 1), space(1), false))
t;提示:您也可以直接使用MaxCompute(ODPS)系统函数序列快速生成序列。
sql - 4
select sequence(1, 3, 1);
result
二、应用场景举例
1) 恢复任意维度组合下的维度列集群名称
在多维分析场景中,可以使用多维数据集、汇总、分组集等高级聚合函数来聚合不同维度组合下的数据统计数据。
场景描述
在现有用户访问日志表中,每行数据代表一个用户访问日志。
sql - 5
with visit_log as (
select stack (
6,2024-01-01', '101', '湖北', '武汉', 'android',2024-01-01', '102', '湖南', '长沙', 'ios',2024-01-01', '103', '四川', '成都', 'windows',2024-01-02', '101', '湖北', '孝感', 'mac',2024-01-02', '102', '湖南', '邵阳', 'android',2024-01-03', '101', '湖北', '武汉', 'ios'
字段:日期、用户、省份、城市、设备类型。
as (dt, user_id, province, city, device_type)
select * from visit_log;
现在,针对省、市、设备类型三个维度列,通过分组集聚合统计,获取不同维度组合下的用户访问次数。 问:
如何知道统计信息是从哪个维度列聚合的?
想要输出聚合维度列的名称如何应对下游报表展示等场景?
解决方案思路:
这可以借助MaxCompute提供的分组ID(ODPS)来解决,核心方法是反向实现分组ID。
具体步骤如下:
准备好所有分组 ID。
生成值的升序序列,将每个值转换为二进制字符串,并展开二进制字符串的每个位。
其中。 所有维度列的数目是所有维度组合的数目,每个数值表示一个分组 ID。
准备好所有维度名称。
生成一个字符串序列,依次保存维度列的名称,即。
dim_name_1, dim_name_2, .dim_name_n }
将分组 ID 映射到维度列名称。
对于分组 id 递增序列中的每个数值,将该值的每个位映射到维度名称序列的下标,并输出与位 0 对应的所有维度名称。 例如:
grouping__id:3 =>
维度名称序列:
映射:分组 ID 为 3 的行的聚合维度为:省份。
SQL 实现:
sql - 6
with group_dimension as (
选择 (SELECT) -- 与每个分组对应的维度字段。
gb.group_id, concat_ws(",", collect_list(case when gb.placeholder_bit = 0 then dim_col.val else null end)) as dimension_name
from (
select groups.pos as group_id, pe.*
from (
select posexplode(split(space(cast(pow(2, 3) as int) -1), space(1), false))
组 -- 所有组。
lateral view posexplode(regexp_extract_all(lpad(conv(groups.pos,10,2), 3, "0"), '(0|1)'PE 作为占位符 IDX,占位位 -- 每个组的位信息。
gbleft 联接 ( 所有维度字段。
select posexplode(split("省、市、设备类型", ','))
dim_col on gb.placeholder_idx = dim_col.pos
group by gb.group_id
select
group_dimension.dimension_name,province, city, device_type,visit_count
from (
select
grouping_id(province, city, device_type) as group_id,province, city, device_type,count(1) as visit_count
from visit_log b
group by province, city, device_type
grouping sets(
province),province, city),province, city, device_type)
tjoin group_dimension on t.group_id = group_dimension.group_id
order by group_dimension.dimension_name;
3. 间隔
区间与序列具有不同的数据特性,但在实际应用中,序列和区间的处理具有更多的共性。 本节将介绍一些常见的间隔方案,并抽象出常见的解决方案。
1. 常用间隔操作
1) 区间分割
数字间隔是已知的。
如何将此区间划分为多个子段?
这个问题可以简化为一系列问题,序列的公式是。
其中。
具体步骤如下:
生成一个长度的数组;
通过 UDTF 函数 posexplode 为数组中的每个元素生成一个索引下标;
取出每个元素的索引下标,计算序列公式,得到每个子区间的起始值和结束值。
SQL 实现:
sql - 7
select
a + t.pos * d as sub interval start, - 子间隔的起始值。
a + t.pos + 1) *d 作为子间隔结束 -- 子间隔的结束值。
from (
select posexplode(split(space(n - 1), space(1), false))
t;2) 间隔交叉
已知两个日期间隔之间存在交叉 ['2024-01-01', '2024-01-03'] 、'2024-01-02', '2024-01-04']。问:
如何合并两个日期间隔并返回到新的合并间隔?
如何知道哪些日期是交叉日期并返回该日期的交叉次数?
有很多方法可以解决这些问题,但这里有一个简单而通用的解决方案。 其核心思想是将序列生成和区间分割的方法结合起来,首先将日期区间分解为最小的处理单元,即由多个日期组成的序列,然后根据日期粒度进行统计。 具体步骤如下:
获取每个日期间隔中包含的天数;
根据日期间隔中包含的天数,将日期间隔拆分为相应数量的升序日期序列;
合并的间隔和交叉次数通过日期序列进行计算。
SQL 实现:
sql - 8
with dummy_table as (
select stack(
as (date_start, date_end)
select
min(date item) 作为日期开始合并,max(date item) 作为日期结束合并,collect set( - 交叉日期计数。
case when date_item_cnt > 1 then concat(date_item, ':', date_item_cnt) else null end
as overlap_date
from (
select
拆卸后的单个日期。
date add(date start, pos) 作为日期项,- 拆解后单个日期的出现次数。
count(1) over (partition by date_add(date_start, pos)) as date_item_cnt
from dummy_table
lateral view posexplode(split(space(datediff(date_end, date_start)),space(1), false)) t as pos, val
t;
让它更难一点!
如果存在多个日期间隔,并且间隔之间的相交状态未知,如何解决上述问题。 即:
如何合并多个日期间隔并在合并后返回多个新间隔?
如何知道哪些日期是交叉日期并返回该日期的交叉次数?
SQL 实现:
sql - 9
with dummy_table as (
select stack(
as (date_start, date_end)
select
min(date item) 作为日期开始合并,max(date item) 作为日期结束合并,collect set( - 交叉日期计数。
case when date_item_cnt > 1 then concat(date_item, ':', date_item_cnt) else null end
as overlap_date
from (
select
拆卸后的单个日期。
date add(date start, pos) 作为日期项,- 拆解后单个日期的出现次数。
count(1) over (partition by date add(date start, pos)) as date item cnt,- 对于拆解后的单个日期,重新组织成新的跨区标签。
date_add(date_add(date_start, pos), 1 - dense_rank() over (order by date_add(date_start, pos)))as cont
from dummy_table
lateral view posexplode(split(space(datediff(date_end, date_start)),space(1), false)) t as pos, val
tgroup by cont;
二、应用场景举例
1) 任何时间段的统计数据
场景描述
现有用户还款计划,表中的一条数据表示用户每天在指定的日期间隔内还款人民币 [开始日期、结束日期]。
sql - 10
with user_repayment as (
select stack(
字段:用户、开始日期、结束日期、每日还款金额。
as (user_id, date_start, date_end, repayment)
select * from user_repayment;
如何计算所有用户在任何时间段(例如2024-01-15至2024-01-16)每天的总还款金额?
解决方案思路:
核心思想是将日期间隔转换为日期序列,然后根据日期序列收集**。
SQL 实现:
sql - 11
select
date_item as day,sum(repayment) as total_repayment
from (
select
date_add(date_start, pos) as date_item,repayment
from user_repayment
lateral view posexplode(split(space(datediff(date_end, date_start)),space(1), false)) t as pos, val
twhere date_item >= '2024-01-15' and date_item <= '2024-01-16'
group by date_item
order by date_item;
四、排列组合
排列和组合是离散数据常用的数据组织方法,本节将介绍排列和组合的实现方法,并通过组合示例重点介绍数据的处理。
1. 常见的排列和组合操作
1) 安排
已知字符序列 ['a', 'b', 'c'] 一次从序列中重复选择 2 个字符,如何获得所有排列?
借助多个横向视图,整体实现相对简单。
sql - 12
select
concat(val1, val2) as perm
from (select split('a,b,c', ',') as characters) dummy
lateral view explode(characters) t1 as val1
lateral view explode(characters) t2 as val2;
2) 组合
已知字符序列 ['a', 'b', 'c'] 一次重复序列中的 2 个字符,如何获得所有组合?
借助多个横向视图,整体实现相对简单。
sql - 13
select
concat(least(val1, val2), greatest(val1, val2)) as comb
from (select split('a,b,c', ',') as characters) dummy
lateral view explode(characters) t1 as val1
lateral view explode(characters) t2 as val2
group by least(val1, val2), greatest(val1, val2);
注意:您也可以使用MaxCompute(ODPS)系统功能组合快速生成组合。
sql - 14
select combinations(array('foo', 'bar', 'boo'),2);
result
foo', 'bar'], 'foo', 'boo']['bar', 'boo']]
二、应用场景举例
1)组别对比统计
场景描述
现有的配送策略转换表,其中包含一段数据,该数据表示配送策略在一天内生成的订单数。
sql - 15
with strategy_order as (
select stack(
3,2024-01-01', 'strategy a', 10,2024-01-01', 'strategy b', 20,2024-01-01', 'strategy c', 30
字段:日期、交货策略、订单量。
as (dt, strategy, order_cnt)
select * from strategy_order;
如何根据发货策略设置成对对比组,按组对比展示不同策略的转化订单量?
解决方案思路:
其核心思想是从所有无重复交割策略的列表中抽取2个策略,生成所有组合结果,然后关联策略顺序表对统计结果进行分组。
SQL 实现:
sql - 16
select /*+ mapjoin(combs) */
combs.strategy_comb,so.strategy,so.order_cnt
from strategy_order so
join ( 生成所有比较组。
select
concat(least(val1, val2), '-', greatest(val1, val2)) as strategy_comb,least(val1, val2) as strategy_1, greatest(val1, val2) as strategy_2
from (
select collect_set(strategy) as strategies
from strategy_order
dummylateral view explode(strategies) t1 as val1
lateral view explode(strategies) t2 as val2
where val1 <>val2
group by least(val1, val2), greatest(val1, val2)
combs on 1 = 1
where so.strategy in (combs.strategy_1, combs.strategy_2)
order by combs.strategy_comb, so.strategy;
5. 连续
本节重点介绍连续性问题,重点介绍常见的顺序活动方案。 针对静态类型和动态类型的连续活动,描述了不同的实现方案。
1. 普通连续活动统计
场景描述
在现有用户访问日志表中,每行数据代表一个用户访问日志。
sql - 17
with visit_log as (
select stack (
6,2024-01-01', '101', '湖北', '武汉', 'android',2024-01-01', '102', '湖南', '长沙', 'ios',2024-01-01', '103', '四川', '成都', 'windows',2024-01-02', '101', '湖北', '孝感', 'mac',2024-01-02', '102', '湖南', '邵阳', 'android',2024-01-03', '101', '湖北', '武汉', 'ios'
字段:日期、用户、省份、城市、设备类型。
as (dt, user_id, province, city, device_type)
select * from visit_log;
如何获得连续 2 天访问的用户?
以上问题都是在连续性分析中获得连续性的结果可能会超过一个固定的阈值,归类于此处有关连续活动大于 n 天阈值的常见连续活动场景的统计信息。
SQL 实现:
基于相邻日期之间的差异(滞后领先版本)。
整体实现相对简单。
sql - 18
select user_id
from (
select,lag(dt, 2 - 1) over (partition by user_id order by dt) as lag_dt
from (select dt, user_id from visit_log group by dt, user_id) t0
t1where datediff(dt, lag_dt) +1 = 2
group by user_id;
根据相邻日期之间的差异实现(排序版本)。
整体实现相对简单。
sql - 19
select user_id
from (
select *,dense_rank() over (partition by user_id order by dt) as dr
from visit_log
t1where datediff(dt, date_add(dt, 1 - dr)) 1 = 2
group by user_id;
根据连续的活动天数实施
可以看作是:根据相邻日期之间的差异实现(排序版本)。,实现可以获取更多信息,例如连续活动天数。
sql - 20
select user_id
from (
select,- 连续活动天数。
count(distinct dt)
over (partition by user_id, cont) as cont_days
from (
select,date_add(dt, 1 - dense_rank()
over (partition by user_id order by dt)) as cont
from visit_log
t1 t2where cont_days >= 2
group by user_id;
基于连续活动间隔
可以看作是:根据相邻日期之间的差异实现(排序版本)。,实现可以获取更多信息,例如连续活动间隔。
sql - 21
select user_id
from (
select
用户 ID, cont, - 连续有效间隔。
min(dt) as cont_date_start, max(dt) as cont_date_end
from (
select,date_add(dt, 1 - dense_rank()
over (partition by user_id order by dt)) as cont
from visit_log
t1group by user_id, cont
t2where datediff(cont_date_end, cont_date_start) +1 >= 2
group by user_id;
2. 动态连续活动统计
场景描述
在现有用户访问日志表中,每行数据代表一个用户访问日志。
sql - 22
with visit_log as (
select stack (
6,2024-01-01', '101', '湖北', '武汉', 'android',2024-01-01', '102', '湖南', '长沙', 'ios',2024-01-01', '103', '四川', '成都', 'windows',2024-01-02', '101', '湖北', '孝感', 'mac',2024-01-02', '102', '湖南', '邵阳', 'android',2024-01-03', '101', '湖北', '武汉', 'ios'
字段:日期、用户、省份、城市、设备类型。
as (dt, user_id, province, city, device_type)
select * from visit_log;
如何获取最长 2 个连续活跃用户数、输出用户数、最长连续活跃天数和最长连续活跃日期段?
在分析连续性时,获得连续性的结果不是也不能与固定阈值进行比较,而是各自使用最长的连续活动作为动态阈值,其分类如下动态和连续活动场景的统计
SQL 实现:
基于常见连续活动场景统计最后的 sql:
sql - 23
select
user id, - 最大连续活动天数。
datediff(max(dt), min(dt)) 1 作为连续天数,- 最长的连续活动日期范围。
min(dt) as cont_date_start, max(dt) as cont_date_end
from (
select,date_add(dt, 1 - dense_rank()
over (partition by user_id order by dt)) as cont
from visit_log
t1group by user_id, cont
order by cont_days desc
limit 2;
6. 扩展
扩展到更复杂的方案是本文前几章内容的组合和变体。
1.区间是连续的(将最长的子区间划分)。
场景描述
现有用户扫描或连接WiFi的用户WiFi日志,每行数据代表用户在特定时间扫描或连接WiFi的日志。
sql - 24
with user_wifi_log as (
select stack (
9,2024-01-01 10:01:00', '101', 'cmcc-starbucks', 'scan'-扫描。
2024-01-01 10:02:00', '101', 'cmcc-starbucks', 'scan',2024-01-01 10:03:00', '101', 'cmcc-starbucks', 'scan',2024-01-01 10:04:00', '101', 'cmcc-starbucks', 'conn'-连接。
2024-01-01 10:05:00', '101', 'cmcc-starbucks', 'conn',2024-01-01 10:06:00', '101', 'cmcc-starbucks', 'conn',2024-01-01 11:01:00', '101', 'cmcc-starbucks', 'conn',2024-01-01 11:02:00', '101', 'cmcc-starbucks', 'conn',2024-01-01 11:03:00', '101', 'cmcc-starbucks', 'conn'
字段:时间、用户、WiFi、状态(扫描、已连接)。
as (time, user_id, wifi, status)
select * from user_wifi_log;
现在需要用户行为分析如何划分用户不同的WiFi行为区间? 满意:
有两种类型的行为:scan、conn;
行为间隔定义为:相同的行为类型,两个相邻行为之间的时间差不超过30分钟;
如果满足定义,则不同的行为间隔应该是最长的;
以上问题稍微复杂一些,可以看出如下:动态连续活动统计最长的连续活动变种。 可以描述为:结合连续性阈值和行为序列中的上下文信息,划分最长的子区间问题。
SQL 实现:
核心逻辑:按用户和 wifi 分组划分行为间隔,结合连续性阈值和行为序列上下文信息。
详细步骤:按用户和wifi分组,在分组窗口中按时间顺序对数据进行排序;
如果两条记录之间的时间差超过 30 分钟,或者两条记录的行为状态(扫描、连接)发生变化,则行为间隔除以临界点。 直到遍历所有记录;
最终输出结果:用户、wifi、行为状态(扫描状态、连接状态)、行为开始时间、行为结束时间;
sql - 25
select
user_id,wifi,max(status) as status,min(time) as start_time,max(time) as end_time
from (
select *,max(if(lag_status is null or lag_time is null or status <>lag_status or datediff(time, lag_time, 'ss') >60 * 30, rn, null))
over (partition by user_id, wifi order by time) as group_idx
from (
select *,row_number() over (partition by user_id, wifi order by time) as rn,lag(time, 1) over (partition by user_id, wifi order by time) as lag_time,lag(status, 1) over (partition by user_id, wifi order by time) as lag_status
from user_wifi_log
t1 t2group by user_id, wifi, group_idx
这种情况下的连续性判别可以扩展到更多的场景,例如基于日期差、时差、枚举类型、距离差等的数据场景。
结论
通过灵活零星的数据处理思维,可以用基础语法来解决复杂的数据场景,这是贯穿本文全文的思想。 本文针对序列生成、区间变换、排列组合、连续判别等常见场景给出了较为通用的解决方案,并结合算例对实际应用进行了说明。
本文试图找到一种独特的方式,强调灵活的数据处理思维,希望让读者感到光明,希望能真正帮助到读者。 同时,毕竟个人能力有限,想法不一定是最优的,甚至可能会出现错误,欢迎提出意见或建议。
作者丨Rigaud.
*丨***阿里开发者(ID:Ali Tech)。