代码改变世界

【原】StreamInsight 浅入浅出(五)—— LINQ 语法

2010-11-23 16:29  拖鞋不脱  阅读(2295)  评论(1编辑  收藏  举报

在上一篇文章中说过,在构建查询模板时,用的语言框架是 LINQ 语言框架,关于 LINQ 的基本概念,可以参看 MSDN http://msdn.microsoft.com/zh-cn/library/bb397926.aspx,总之就是采用了一种类似 SQL 的方便快捷的语法,用于对数据集进行查询、筛选等一系列操作的语言框架。

关键词

虽然 LINQ 语法总体上是类似的,但在不同的系统下,还有细微的差别,尤其是对于一些原生关键字的支持会有所不同。在 StreamInsight 中,LINQ 语法中支持的原生关键字包括(后面的 URL 是 MSDN 中的说明):

这里值得一提的是分组,因为我们知道,在查询模板,输入、输出都是 CepStream 形式的,而一个 CepStream 形式的流经过 select、where、join…on 等操作后依然是 CepStream 形式的流。但经过分组,也就是 group…by 操作后,CepStream 形式的流会变为 CepGroupingStreams 形式的一种组结构,再通过 into 操作会化为 CepWindow 形式的窗口:

var deviceGroups = from e in sensorStream
   group e by e.sensorId;

deviceGroups 是 CepGroupingStreams 形式的。

var result = from oneMinReading in sensorStream
            group oneMinReading by oneMinReading.SensorId into eachGroup...

这里 eachGroup 是CepWindow 形式的,注意这一条语句并不完整,如果直接在 eachGroup 后面结束语句,会报编译错误,因为语法要求查询语句必须以 select 子句或者 group 子句完结。

而为了把 CepGroupingStreams 或者 CepWindow 转化为可用的 CepStream,则要根据实际需要应用 ApplyWithUnion 或者各种窗口函数。其中 ApplyWithUnion 是有两个参数的方法,第一个参数是对单一分组的操作,比如求和、求平均,它可以被拆分为一个独立的方法,输入与输出都是 CepStream 类型;第二个参数则指定了最终输出的 CepStream 流的形状(包含的字段及意义)。

时间戳

事件的时间戳在 StreamInsight 系统中直接影响了该事件的有效范围,尤其是对于事件窗口,修改了事件的时间戳,能控制该事件在一个事件窗口中是否出现。LINQ 提供了四个扩展方法改变事件的时间戳,其实就是修改流中事件的起止时间(StartTime, EndTime)。

  • ShiftEventTime()
  • AlterEventDuration()
  • AlterEventLifetime()
  • ToPointEventStream()

这里需要注意的是 AlterEventLifetime 方法中,第一个参数返回的是事件的起始时间,第二个参数返回的是事件的持续时间(Duration),而不是事件的结束时间(EndTime)。关于这四个方法的具体示例,请参看 http://technet.microsoft.com/zh-cn/library/ee362414.aspx

事件窗口

事件窗口是 StreamInsight 中非常重要的概念,也是最容易让人糊涂的。它广泛应用于各种分组聚合中,直接影响了各种聚合运算的结果。

所谓事件窗口,可以认为是一些事件的集合。同一事件窗口中的事件的共同点是他们处于同一个时间段中,或者说同一个时间窗口中。假设有一个巨大的海底水族馆,所有的事件就是在水族馆里的鱼,水族馆壁的每一块玻璃就是一个时间窗口,那么我们在一个玻璃中看到的鱼就可以认为是这个时间窗口中包含的事件,也就是一个事件窗口。而且,由于鱼是有长度的,就像事件也是有持续时间的,我们在相邻的玻璃里能看到同一条鱼,那么在相邻的事件窗口中也会包含同一个事件。

image

事件窗口包含如下的窗口类型:

  • 计数窗口
  • 跳跃窗口
  • 快照窗口

计数窗口

计数窗口的窗口大小不定,受指定的计数大小 N 影响。假设所有事件的开始时间不同,则第一个计数窗口从第一个事件开始时开始,到第 N 个事件开始 + 一个时钟周期时结束,而第 N 个事件开始时第二个计数窗口开始。所以,第 N 个事件必然是要跨两个计数窗口的

Ff518595.41e46c59-855a-486a-a105-8f510f041578(zh-cn,SQL.105)

而且,上述表述的前提是所有事件的开始时间不同。如果有事件的开始时间相同,则计数窗口实际是对事件的不同的开始时间计数,而非对事件计数。也就是说,如果两个事件的开始时间相同,对于计数窗口来说,它们发生时增加的计数只是1,而不是2。

Ff518595.47b9b8d9-7470-4fdb-8a93-b323bf82b66b(zh-cn,SQL.105)

(由于第三第四个事件发生时间相同,所以在计数窗口中只作为一个单位计数。)

计数窗口的定义如下:

var agg = from w in inputStream.CountByStartTimeWindow(10, CountWindowOutputPolicy.PointAlignToWindowEnd) 
select new { sum = w.Sum(e => e.i) };

快照窗口

快照窗口没有像计数窗口那样预定的计数,也没有跳跃窗口那样预定的窗口大小和跳跃大小,完全由事件的发生和结束来开启和关闭。所有事件的开始都会开启一个新的窗口并关闭旧的窗口,而所有事件的结束都会关闭现有的窗口。也就是说所有事件的开始时间和结束时间都位于窗口边界,窗口由事件驱动。

Ff518550.0b733404-b938-439c-b1eb-ba51978a94c9(zh-cn,SQL.105)

当流发生改变(新事件产生、旧事件结束)时,都会切割出窗口,相当于对流的状态做了一个快照,这也就是快照窗口名称的含义。

快照窗口的定义如下:

var snapshotAgg = from w in inputStream.SnapshotWindow(SnapshotWindowOutputPolicy.Clip)
                  select new { sum = w.Sum(e => e.i) };

跳跃窗口

跳跃窗口的窗口大小(WindowSize)和跳跃大小(HopSize)固定的一种窗口,和计数窗口、快照窗口由事件驱动不同,跳跃窗口的开启和关闭并不受事件的开始、结束影响。它会每过 HopSize 的时间就创建一个大小为 WindowSize 的窗口,所以,可能会有窗口的重叠,或者连续窗口间的间隙存在。当且仅当 HopSize = WindowSize 时,事件窗口是无缝且不重叠的,称为“翻转窗口”。

Ff518448.33940f39-2c3f-4a11-91a8-8dd8d5e8bee7(zh-cn,SQL.105)

跳跃窗口的定义如下:

var hoppingAgg = from w in inputStream.HoppingWindow(TimeSpan.FromHours(1),
                                                     TimeSpan.FromMinutes(10),
                                                     HoppingWindowOutputPolicy.ClipToWindowEnd)
                 select new { sum = w.Sum(e => e.i) };

翻转窗口的简化定义如下:

var tumblingAgg = from w in inputStream.TumblingWindow(TimeSpan.FromHours(1),
                                                       HoppingWindowOutputPolicy.ClipToWindowEnd)
                  select new { sum = w.Sum(e => e.i) };

跳跃窗口的起始时间不受事件的影响,那么第一个跳跃事件是什么时候开始的呢?在跳跃窗口里有一套默认的对齐策略:如果跳跃大小是1分钟,则第一个窗口会在第一个事件开始时间的整分开始;如果跳跃大小是1小时,则第一个窗口在整点开始;如果大小是24小时,则在零点开始。也可以通过指定对齐方式改变窗口的开始时间:

var snapshotAgg = from w in inputStream.TumblingWindow(
                         TimeSpan.FromHours(24),
                         new DateTime(TimeSpan.FromHours(9).Ticks, DateTimeKind.Utc),
                         HoppingWindowOutputPolicy.ClipToWindowEnd)
                  select new { sum = w.Sum(e => e.i) };

上述例子就将跳跃大小为24小时的窗口的开始时间设为了每天的9点。

此外,跳跃窗口对于 CTI 事件的时间戳也有影响,所有 CTI 事件都将移到所属窗口的开头。

聚合与 TopK

事件窗口是事件的集合,对于集合做的操作包括如下两种:

  • 聚合
  • TopK

其中聚合(avg,sum,min,max,count)应用于 CepWindowStream 类型的对象,并返回一个标量值,以投影为输出的负载字段。详情请参见 http://technet.microsoft.com/zh-cn/library/ee362304.aspx

TopK 用于从一个事件窗口中取出 N 个事件,所以 TopK 返回的会是零个或者多个事件,而且 TopK 不能应用于计数窗口。详情请参见 http://technet.microsoft.com/zh-cn/library/ee391229.aspx

窗口策略

在跳跃窗口中,提到我们可以指定窗口的对齐策略,改变窗口的开始时间。事实上,对于三种窗口类型,都可以指定窗口的输入、输出策略,从而确定输入事件与输出结果的时间对齐方式。

Ee842704.41f894eb-2367-4d63-9f09-cbc8160ab2aa(zh-cn,SQL.105)

但目前 StreamInsight 中暂时只支持 WindowInputPolicy.ClipToWindow 这一输入策略,而对于三种事件窗口,也只分别提供了仅一种输出策略:

  • SnapshotWindowOutputPolicy.Clip 输出事件的结束时间将剪辑到窗口的结束时间
  • HoppingWindowOutputPolicy.ClipToWindowEnd 输出事件的结束时间将剪辑到窗口的结束时间
  • CountWindowOutputPolicy.PointAlignToWindowEnd 得到的事件将转换为窗口末尾的点事件

而且输入策略参数是可选的,而输出策略参数是必须指定的,所以当定义事件窗口时,会发现输出策略这个参数必须添,但却只能添一种参数, MSDN 的说法是“encouraging the user's awareness of the potential adjustment of the resulting events”……

其他

除了以上关于 LINQ 的介绍外,还有一点是必须要提一下的:在 .Net 3.5 下,是可以直接投影匿名类的,也就是可以直接

var queryProject = from c in TestEventStream
                   select new {j = c.i * 2, k = c.f * 2};

select 出来的可以是未定义的类。

但在 .Net 4 下,这样会出错,原因不明。解决办法就是事先定义要输出的类。

class TempClass
{
    public double j
    {
        get;
        set;
    }

    public double k
    {
        get;
        set;
    }
}

然后再投影:

var queryProject = from c in TestEventStream select new TempClass {j = c.i * 2, k = c.f * 2};