Blink SQL窗口函数

概述

窗口函数

窗口函数Flink SQL支持基于无限大窗口的聚合(无需在SQL Query中,显式定义任何窗口)以及对一个特定的窗口的聚合。例如,需要统计在过去的1分钟内有多少用户点击了某个的网页,可以通过定义一个窗口来收集最近1分钟内的数据,并对这个窗口内的数据进行计算。

Flink SQL支持的窗口聚合主要是两种:

  • Window聚合
  • Over聚合

Window聚合支持Event Time和Processing Time两种时间属性定义窗口。

每种时间属性类型支持三种窗口类型:滚动窗口(TUMBLE)、滑动窗口(HOP)和会话窗口(SESSION)。

时间属性

Flink SQL支持以下两种时间属性。

  • Event Time:事件时间(通常是数据的最原始的创建时间),Event Time一定是提供在Schema里面的数据。
  • Processing Time:对事件进行处理的本地系统时间。

级联窗口

Rowtime列在经过窗口操作后,其Event Time属性将丢失。可以使用辅助函数TUMBLE_ROWTIMEHOP_ROWTIMESESSION_ROWTIME获取窗口中的Rowtime列的最大值max(rowtime)作为时间窗口的Rowtime,其类型是具有Rowtime属性的TIMESTAMP,取值为window_end - 1。例如[00:00, 00:15]的窗口,返回值为00:14:59:999

示例逻辑:基于1分钟的滚动窗口聚合结果,进行1小时的滚动窗口聚合。

CREATE TABLE user_clicks(
	username VARCHAR,
  click_url VARCHAR,
  ts TIMESTAMP,
  WATERMARK wk FOR ts AS withOffset(ts, 2000)	-- 为Rowtime定义WaterMark
) with (
  type='datahub',
  ...
);

CREATE TABLE tumble_output(
	window_start TIMESTAMP,
  window_end TIMESTAMP,
  username VARCHAR,
  clicks BIGINT
)	with (
  type='print'
);

CREATE VIEW one_minute_window_output AS 
SELECT
	// 使用TUMBLE_ROWTIME作为二级Window的聚合时间
	TUMBLE_ROWTIME(ts, INTERVAL '1' MINUTE) AS rowtime,
	username,
	COUNT(click_url) AS cnt
FROM user_clicks
GROUP BY TUMBLE(ts, INTERVAL '1' MINUTE), username
;

INSERT INTO tumble_output
SELECT
	TUMBLE_START(rowtime, INTERVAL '1' HOUR),
	TUMBLE_END(rowtime, INTERVAL '1' HOUR),
	username,
	SUM(cnt)
FROM one_minute_window_output
GROUP BY TUMBLE(rowtime, INTERVAL '1' HOUR), username
;

滚动窗口

滚动窗口(TUMBLE)将每个元素分配到一个指定大小的窗口中。通常,滚动窗口是固定大小的,且不会重叠。例如:指定了一个5分钟大小的滚动窗口,无限流的数据会根据时间划分为[0:00, 0:05)[0:05, 0:10)[0:10, 0:15)等窗口。如下展示了一个30秒的滚动窗口。

img

语法

TUMBLE函数用在GROUP BY子句中,用来定义滚动窗口。

TUMBLE(<time-attr>, <size-interval>)
<size-interval>: INTERVAL 'string' timeUnit

参数必须是时间流中的一个合法的时间属性字段,指定为Processing Time或Event Time。

标识函数

使用标识函数选出窗口的起始时间或结束时间,窗口的时间属性用于下级Window的聚合。

窗口标识函数返回类型描述
TUMBLE_START(time-attr, size-interval)TIMESTAMP返回窗口的其实时间(包含边界)。例如[00:10,00:15]窗口,返回00:10
TUMBLE_END(time-attr, size-interval)TIMESTAMP返回窗口的结束时间(包含边界)。例如[00:00, 00:15]窗口,返回00:15
TUMBLE_ROWTIME(time-attr, size-interval)TIMESTAMP(rowtime-attr)返回窗口的结束时间(不包含边界)。例如(00:00, 00:15)窗口,返回00:14:59.999。返回值是一个rowtime attribute,即可以基于该字段做时间属性的操作。
TUMBLE_PROCTIME(time-attr, size-interval)TIMESTAMP(rowtime-attr)返回窗口的结束时间(不包含边界)。例如(00:00, 00:15)窗口,返回00:14:59.999。返回值是一个Proctime Attribute,即可以基于该字段做时间属性的操作。

使用Event Time统计每个用户每分钟在指定网站的单击数示例

测试数据
username(VARCHAR)click_url(VARCHAR)ts(TIMESTAMP)
Jarkhttp://taobao.com/xxx2017-10-10 10:00:00.0
Jarkhttp://taobao.com/xxx2017-10-10 10:00:10.0
Jarkhttp://taobao.com/xxx2017-10-10 10:00:49.0
Jarkhttp://taobao.com/xxx2017-10-10 10:01:05.0
Jarkhttp://taobao.com/xxx2017-10-10 10:01:58.0
Timohttp://taobao.com/xxx2017-10-10 10:02:10.0
测试语句
CREATE TABLE user_clicks(
  username varchar,
  click_url varchar,
  ts timeStamp,
  WATERMARK wk FOR ts as withOffset(ts, 2000) --为rowtime定义Watermark。
) with (
  type='datahub',
  ...
);

CREATE TABLE tumble_output(
  window_start TIMESTAMP,
  window_end TIMESTAMP,
  username VARCHAR,
  clicks BIGINT
) with (
  type='RDS'
);

INSERT INTO tumble_output
SELECT
TUMBLE_START(ts, INTERVAL '1' MINUTE) as window_start,
TUMBLE_END(ts, INTERVAL '1' MINUTE) as window_end,
username,
COUNT(click_url)
FROM user_clicks
GROUP BY TUMBLE(ts, INTERVAL '1' MINUTE), username;
测试结果
window_start (TIMESTAMP)window_end (TIMESTAMP)username(VARCHAR)clicks(BIGINT)
2017-10-10 10:00:00.02017-10-10 10:01:00.0Jark3
2017-10-10 10:01:00.02017-10-10 10:02:00.0Jark2
2017-10-10 10:02:00.02017-10-10 10:03:00.0Timo1

使用Processing Time统计每个用户每分钟在指定网站的单击数示例

测试数据
username (VARCHAR)click_url(VARCHAR)
Jarkhttp://taobao.com/xxx
Jarkhttp://taobao.com/xxx
Jarkhttp://taobao.com/xxx
Jarkhttp://taobao.com/xxx
Jarkhttp://taobao.com/xxx
Timohttp://taobao.com/xxx
测试语句
CREATE TABLE window_test (
  username   VARCHAR,
  click_url  VARCHAR,
  ts as PROCTIME()
) WITH (
  type='datahub',
  ...
);

CREATE TABLE tumble_output(
  window_start TIMESTAMP,
  window_end TIMESTAMP,
  username VARCHAR,
  clicks BIGINT
) with (
  type='print'
);

INSERT INTO tumble_output
SELECT
TUMBLE_START(ts, INTERVAL '1' MINUTE),
TUMBLE_END(ts, INTERVAL '1' MINUTE),
username,
COUNT(click_url)
FROM window_test
GROUP BY TUMBLE(ts, INTERVAL '1' MINUTE), username;
测试结果
window_start (TIMESTAMP)window_end (TIMESTAMP)username (VARCHAR)clicks(BIGINT)
2019-04-11 14:43:00.0002019-04-11 14:44:00.000Jark5
2019-04-11 14:43:00.000 2019-04-11 14:44:00.000Timo1

因为本地调试是瞬时的,处理时间可能小于1秒,所以使用Processing Time时间属性对数据进行窗口聚合,可能会出现本地调试没有结果的情况。

滑动窗口

实时计算滑动窗口(HOP)暂不支持与LAST_VALUE、FIRST_VALUE或TopN函数共同使用。

什么是滑动窗口

滑动窗口(HOP),也被称作Sliding Window。不同于滚动窗口,滑动窗口的窗口可以重叠。

滑动窗口有两个参数:slidesizeslide为每次滑动的步长,size为窗口的大小。

  • slide < size,则窗口会重叠,每个元素会被分配到多个窗口。
  • slide = size,则等同于滚动窗口(TUMBLE)。
  • slide > size,则为跳跃窗口,窗口之间不重叠且有间隙。

通常,大部分元素符合多个窗口情景,窗口是重叠的。因此,滑动窗口在计算移动平均数(moving averages)时很实用。例如,计算过去5分钟数据的平均值,每10秒钟更新一次,可以设置slide为10秒,size为5分钟。下图为您展示间隔为30秒,窗口大小为1分钟的滑动窗口。

滑动窗口

滑动窗口函数语法

HOP函数用在group by子句中,用来定义滑动窗口。

HOP(<time-attr>, <slide-interval>,<size-interval>)
<slide-interval>: INTERVAL 'string' timeUnit
<size-interval>: INTERVAL 'string' timeUnit            

<time-attr>参数必须是流中的一个合法的时间属性字段,指定为Processing Time或Event Time。

滑动窗口标识函数

使用滑动窗口标识函数选出窗口的起始时间或者结束时间,窗口的时间属性用于下级Window的聚合。

窗口标识函数返回类型描述
HOP_START(<time-attr>, <slide-interval>, <size-interval>)TIMESTAMP返回窗口的起始时间(包含边界)。例如[00:10, 00:15) 窗口,返回00:10
HOP_END(<time-attr>, <slide-interval>, <size-interval>)TIMESTAMP返回窗口的结束时间(包含边界)。例如[00:00, 00:15) 窗口,返回00:15
HOP_ROWTIME(<time-attr>, <slide-interval>, <size-interval>)TIMESTAMP(rowtime-attr)返回窗口的结束时间(不包含边界)。例如(00:00, 00:15) 窗口,返回00:14:59.999。返回值是一个rowtime attribute,即可以基于该字段做时间类型的操作。
HOP_PROCTIME(<time-attr>, <slide-interval>, <size-interval>)TIMESTAMP(rowtime-attr)返回窗口的结束时间(不包含边界)。例如(00:00, 00:15) 窗口,返回00:14:59.999 。返回值是一个proctime attribute,即可以基于该字段做时间类型的操作。

示例

统计每个用户过去1分钟的单击次数,每30秒更新1次,即1分钟的窗口,30秒滑动1次。

测试数据
username(VARCHAR)click_url(VARCHAR)ts(TIMESTAMP)
Jarkhttp://taobao.com/xxx2017-10-10 10:00:00.0
Jarkhttp://taobao.com/xxx2017-10-10 10:00:10.0
Jarkhttp://taobao.com/xxx2017-10-10 10:00:49.0
Jarkhttp://taobao.com/xxx2017-10-10 10:01:05.0
Jarkhttp://taobao.com/xxx2017-10-10 10:01:58.0
Timohttp://taobao.com/xxx2017-10-10 10:02:10.0
测试语句
CREATE TABLE user_clicks (
    username VARCHAR,
    click_url VARCHAR,
    ts TIMESTAMP,
    WATERMARK wk FOR ts AS WITHOFFSET (ts, 2000)--为rowtime定义Watermark。
) WITH ( TYPE = 'datahub',
        ...);

CREATE TABLE hop_output (
    window_start TIMESTAMP,
    window_end TIMESTAMP,
    username VARCHAR,
    clicks BIGINT
) WITH (TYPE = 'rds',
        ...);

INSERT INTO
    hop_output
SELECT
    HOP_START (ts, INTERVAL '30' SECOND, INTERVAL '1' MINUTE),
    HOP_END (ts, INTERVAL '30' SECOND, INTERVAL '1' MINUTE),
    username,
    COUNT (click_url)
FROM
    user_clicks
GROUP BY
    HOP (ts, INTERVAL '30' SECOND, INTERVAL '1' MINUTE),
    username                   
测试结果
window_start (TIMESTAMP)window_end (TIMESTAMP)username (VARCHAR)clicks (BIGINT)
2017-10-10 09:59:30.02017-10-10 10:00:30.0Jark2
2017-10-10 10:00:00.02017-10-10 10:01:00.0Jark3
2017-10-10 10:00:30.02017-10-10 10:01:30.0Jark2
2017-10-10 10:01:00.02017-10-10 10:02:00.0Jark2
2017-10-10 10:01:30.02017-10-10 10:02:30.0Jark1
2017-10-10 10:02:00.02017-10-10 10:03:00.0Timo1
2017-10-10 10:02:30.02017-10-10 10:03:30.0Timo1

HOP窗口无法读取数据进入的时间,第一个窗口的开启时间会前移。前移时长=窗口时长-滑动步长,示例如下表。

窗口时长(秒)滑动步长(秒)Event Time第一个窗口StartTime第一个窗口EndTime
120302019-07-31 10:00:00.02019-07-31 09:58:30.02019-07-31 10:00:30.0
60102019-07-31 10:00:00.02019-07-31 09:59:10.02019-07-31 10:00:10.0

会话窗口

什么是会话窗口

会话窗口(SESSION)通过SESSION活动来对元素进行分组。会话窗口与滚动窗口和滑动窗口相比,没有窗口重叠,没有固定窗口大小。相反,当它在一个固定的时间周期内不再收到元素,即会话断开时,该窗口就会关闭。

会话窗口通过一个间隔时间(Gap)来配置,这个间隔定义了非活跃周期的长度。例如,一个表示鼠标单击活动的数据流可能具有长时间的空闲时间,并在两段空闲之间散布着高浓度的单击。如果数据在指定的间隔(Gap)之后到达,则会开始一个新的窗口。

会话窗口示例如下图,每个Key由于不同的数据分布,形成了不同的Window。

img

会话窗口函数语法

SESSION函数用于在GROUP BY子句中定义会话窗口。

SESSION(<time-attr>, <gap-interval>)
<gap-interval>: INTERVAL 'string' timeUnit

<time-attr> 参数必须是数据流中的一个合法的时间属性字段,指定为Processing Time或Event Time。

会话窗口标识函数

使用标识函数选出窗口的起始时间或者结束时间,窗口的时间属性用于下级Window的聚合。

窗口标识函数返回类型描述
SESSION_START(<time-attr>, <gap-interval>)Timestamp返回窗口的起始时间(包含边界)。例如[00:10,00:15]的窗口,返回00:10,即为此会话窗口内第一条记录的时间。
SESSION_END(<time-attr>, <gap-interval>)Timestamp返回窗口的结束时间(包含边界)。例如[00:00,00:15]的窗口,返回 00:15,即为此会话窗口内最后一条记录的时间+<gap-interval>
SESSION_ROWTIME(<time-attr>, <gap-interval>)Timestamp(rowtime-attr)返回窗口的结束时间(不包含边界)。例如(00:00,00:15)的窗口,返回00:14:59.999 。返回值是一个rowtime attribute,也就是可以基于该字段进行时间类型的操作。该参数只能用于基于Event Time的Window。
SESSION_PROCTIME(<time-attr>, <gap-interval>)Timestamp(rowtime-attr)返回窗口的结束时间(不包含边界)。例如(00:00,00:15)的窗口,返回 00:14:59.999 。返回值是一个Proctime Attribute,也就是可以基于该字段进行时间类型的操作。该参数只能用于基于Processing Time的Window。

示例

统计每个用户在每个活跃会话期间的单击次数,会话超时时长为30秒。

测试数据
username (VARCHAR)click_url (VARCHAR)ts (TIMESTAMP)
Jarkhttp://taobao.com/xxx2017-10-10 10:00:00.0
Jarkhttp://taobao.com/xxx2017-10-10 10:00:10.0
Jarkhttp://taobao.com/xxx2017-10-10 10:00:49.0
Jarkhttp://taobao.com/xxx2017-10-10 10:01:05.0
Jarkhttp://taobao.com/xxx2017-10-10 10:01:58.0
Timohttp://taobao.com/xxx2017-10-10 10:02:10.0
测试语句
CREATE TABLE user_clicks(
username varchar,
click_url varchar,
ts timeStamp,
WATERMARK wk FOR ts as withOffset(ts, 2000) -- 为rowtime定义watermark
) with (
type='datahub',
...
);

CREATE TABLE session_output(
window_start TIMESTAMP,
window_end TIMESTAMP,
username VARCHAR,
clicks BIGINT
) with (
type='rds'...
);

INSERT INTO session_output
SELECT
SESSION_START(ts, INTERVAL '30' SECOND),
SESSION_END(ts, INTERVAL '30' SECOND),
username,
COUNT(click_url)
FROM user_clicks
GROUP BY SESSION(ts, INTERVAL '30' SECOND), username;
测试结果
window_start (TIMESTAMP)window_end (TIMESTAMP)username (VARCHAR)clicks (BIGINT)
2017-10-10 10:00:00.02017-10-10 10:00:40.0Jark2
2017-10-10 10:00:49.02017-10-10 10:01:35.0Jark2
2017-10-10 10:01:58.02017-10-10 10:02:28.0Jark1
2017-10-10 10:02:10.02017-10-10 10:02:40.0Timo1

OVER窗口

OVER窗口(OVER Window)是传统数据库的标准开窗,不同于Group By Window,OVER窗口中每1个元素都对应1个窗口。OVER窗口可以按照实际元素的行或实际的元素值(时间戳值)确定窗口,因此流数据元素可能分布在多个窗口中。

在应用OVER窗口的流式数据中,每1个元素都对应1个OVER窗口。每1个元素都触发1次数据计算,每个触发计算的元素所确定的行,都是该元素所在窗口的最后1行。在实时计算的底层实现中,OVER窗口的数据进行全局统一管理(数据只存储1份),逻辑上为每1个元素维护1个OVER窗口,为每1个元素进行窗口计算,完成计算后会清除过期的数据。

语法

SELECT
    agg1(col1) OVER (definition1) AS colName,
    ...
    aggN(colN) OVER (definition1) AS colNameN
FROM Tab1;
  • agg1(col1):按照GROUP BY指定col1列对输入数据进行聚合计算。
  • OVER (definition1):OVER窗口定义。
  • AS colName:别名。
  • agg1到aggN所对应的OVER definition1必须相同。
  • 外层SQL可以通过AS的别名查询数据。

类型

Flink SQL中对OVER窗口的定义遵循标准SQL的定义语法,传统OVER窗口没有对其进行更细粒度的窗口类型命名划分。按照计算行的定义方式,OVER Window可以分为以下两类:

  • ROWS OVER Window:每1行元素都被视为新的计算行,即每1行都是一个新的窗口。
  • RANGE OVER Window:具有相同时间值的所有元素行视为同一计算行,即具有相同时间值的所有行都是同一个窗口。

属性

正交属性说明proctimeeventtime
ROWS OVER Window按照实际元素的行确定窗口。支持支持
RANGE OVER Window按照实际的元素值(时间戳值)确定窗口。支持支持

Rows OVER Window语义

窗口数据

ROWS OVER Window的每个元素都确定一个窗口。ROWS OVER Window分为Unbounded(无界流)和Bounded(有界流)两种情况。

Unbounded ROWS OVER Window数据示例如下图所示。

img

虽然上图所示窗口user1的w7、w8及user2的窗口w3、w4都是同一时刻到达,但它们仍然在不同的窗口,这一点与RANGE OVER Window不同。

Bounded ROWS OVER Window数据以3个元素(往前2个元素)的窗口为例,如下图所示。

img

虽然上图所示窗口user1的w5、w6及user2的窗口w1、w2都是同一时刻到达,但它们仍然在不同的窗口,这一点与RANGE OVER Window不同。

窗口语法
SELECT
    agg1(col1) OVER(
     [PARTITION BY (value_expression1,..., value_expressionN)]
     ORDER BY timeCol
     ROWS 
     BETWEEN (UNBOUNDED | rowCount) PRECEDING AND CURRENT ROW) AS colName, ...
FROM Tab1;       
  • value_expression:分区值表达式。
  • timeCol:元素排序的时间字段。
  • rowCount:定义根据当前行开始向前追溯几行元素。
案例

以Bounded ROWS OVER Window场景为例。假设,一张商品上架表,包含有商品ID、商品类型、商品上架时间、商品价格数据。要求输出在当前商品上架之前同类的3个商品中的最高价格。

测试数据
商品ID商品类型上架时间销售价格
ITEM001Electronic2017-11-11 10:01:0020
ITEM002Electronic2017-11-11 10:02:0050
ITEM003Electronic2017-11-11 10:03:0030
ITEM004Electronic2017-11-11 10:03:0060
ITEM005Electronic2017-11-11 10:05:0040
ITEM006Electronic2017-11-11 10:06:0020
ITEM007Electronic2017-11-11 10:07:0070
ITEM008Clothes2017-11-11 10:08:0020
测试代码
CREATE TABLE tmall_item(
   itemID VARCHAR,
   itemType VARCHAR,
   onSellTime TIMESTAMP,
   price DOUBLE,
   WATERMARK onSellTime FOR onSellTime as withOffset(onSellTime, 0)
) 
WITH (
  type = 'sls',
   ...
);

SELECT
    itemID,
    itemType,
    onSellTime,
    price,  
    MAX(price) OVER (
        PARTITION BY itemType 
        ORDER BY onSellTime 
        ROWS BETWEEN 2 preceding AND CURRENT ROW) AS maxPrice
  FROM tmall_item;
测试结果
temIDitemTypeonSellTimepricemaxPrice
ITEM001Electronic2017-11-11 10:01:002020
ITEM002Electronic2017-11-11 10:02:005050
ITEM003Electronic2017-11-11 10:03:003050
ITEM004Electronic2017-11-11 10:03:006060
ITEM005Electronic2017-11-11 10:05:004060
ITEM006Electronic2017-11-11 10:06:002060
ITEM007Electronic2017-11-11 10:07:007070
ITEM008Clothes2017-11-11 10:08:002020

RANGE OVER Window语义

窗口数据

RANGE OVER Window所有具有共同元素值(元素时间戳)的元素行确定一个窗口,RANGE OVER Window分为Unbounded和Bounded的两种情况。

Unbounded RANGE OVER Window数据示例如下图所示。

img

上图所示窗口user1的w7、user2的窗口w3,两个元素同一时刻到达,属于相同的window,这一点与ROWS OVER Window不同。

Bounded RANGE OVER Window数据,以3秒中数据(INTERVAL '2' SECOND)的窗口为例,如下图所示。

img

上图所示窗口user1的w6、user2的窗口w3,元素都是同一时刻到达,属于相同的window,这一点与ROWS OVER Window不同。

窗口语法
SELECT
    agg1(col1) OVER(
     [PARTITION BY (value_expression1,..., value_expressionN)]
     ORDER BY timeCol
     RANGE 
     BETWEEN (UNBOUNDED | timeInterval) PRECEDING AND CURRENT ROW) AS colName,
...
FROM Tab1;
  • value_expression:进行分区的字表达式。
  • timeCol:元素排序的时间字段。
  • timeInterval:定义根据当前行开始向前追溯指定时间的元素行。
案例

Bounded RANGE OVER Window场景示例:假设一张商品上架表,包含有商品ID、商品类型、商品上架时间、商品价格数据。需要求比当前商品上架时间早2分钟的同类商品中的最高价格。

测试数据
商品ID商品类型上架时间销售价格
ITEM001Electronic2017-11-11 10:01:0020
ITEM002Electronic2017-11-11 10:02:0050
ITEM003Electronic2017-11-11 10:03:0030
ITEM004Electronic2017-11-11 10:03:0060
ITEM005Electronic2017-11-11 10:05:0040
ITEM006Electronic2017-11-11 10:06:0020
ITEM007Electronic2017-11-11 10:07:0070
ITEM008Clothes2017-11-11 10:08:0020
测试代码
CREATE TABLE tmall_item(
   itemID VARCHAR,
   itemType VARCHAR,
   onSellTime TIMESTAMP,
   price DOUBLE,
   WATERMARK onSellTime FOR onSellTime as withOffset(onSellTime, 0)
) 
WITH (
  type = 'sls',
   ...
);

SELECT  
    itemID,
    itemType, 
    onSellTime, 
    price,  
    MAX(price) OVER (
        PARTITION BY itemType 
        ORDER BY onSellTime 
        RANGE BETWEEN INTERVAL '2' MINUTE preceding AND CURRENT ROW) AS maxPrice
  FROM tmall_item;          
测试结果
itemIDitemTypeonSellTimepricemaxPrice
ITEM001Electronic2017-11-11 10:01:002020
ITEM002Electronic2017-11-11 10:02:005050
ITEM003Electronic2017-11-11 10:03:003050
ITEM004Electronic2017-11-11 10:03:006060
ITEM005Electronic2017-11-11 10:05:004060
ITEM006Electronic2017-11-11 10:06:002040
ITEM007Electronic2017-11-11 10:07:007070
ITEM008Clothes2017-11-11 10:08:002020

参考文献:https://help.aliyun.com/document_detail/62492.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值