Esper學習之七:EPL語法(三)

上篇說到了Select Clause和From Clause,今天這篇就說說Aggregation,Group by,Having和Output Clause。先預告一下,由於例子比較多,所以篇幅會有些長,需要各位耐心觀看哦。


1.Aggregation

和SQL一樣,EPL也有Aggregation,即聚合函數。語法如下:

[plain] view plaincopy
  1. aggregate_function([all|distinct] expression)  
aggregate_function就是聚合函數的名字,比如avg,sum等。expression通常是事件流的某個屬性,也可以是不同事件流的多個屬性,或者是屬性和常量、函數之間的運算。舉例如下。

[plain] view plaincopy
  1. // 查詢最新5秒的Apple的平均價格  
  2. select avg(price) as aPrice from Apple.win:time(5 sec)  
  3.   
  4. // 查詢最新10個Apple的價格總和的兩倍  
  5. select sum(price*2) as sPrice from Apple.win:length(10)  
  6.   
  7. // 查詢最新10個Apple的價格,並用函數計算後再算平均值  
  8. select avg(Compute.getResult(price)) from Apple.win:length(10)  
函數只能是靜態方法,普通方法不可用。即使是事件流裏包含的靜態方法,也必須用“類名.方法名”的方式進行引用。

可以使用distinct關鍵字對expression加以約束,表示去掉expression產生的重複的值。默認情況下爲all關鍵字,即所有的expression值都參與聚合運算。例如:

[plain] view plaincopy
  1. // 查詢最新5秒的Apple的平均價格  
  2. select avg(distinct price) as aPrice from Apple.win:time(5 sec)  
  3.   
  4. // 假如:5秒內進入了三個Apple事件,price分別爲2,1,2。則針對該EPL的平均值爲(2+1)/2=1.5。因爲有distinct的修飾,所以第二個2不參與運算,事件總數即爲2,而不是3。  
以上就是聚合函數的使用方法,除此之外需要注意一下幾點

1.聚合函數能用於Select和Having,但是不能用於Where

2.sum,avg,media,stddev,avedev只能計算數值,至於media,stddev和avedev代表什麼意思,請自行百度。

3.Esper會忽略expression爲null不讓參與聚合運算,但是count函數除外,即使是null也認爲是一個事件。如果事件流集合中沒有包含任何事件,或者包含的事件中用於聚合計算的expression都是null(比如收集5秒內進入的事件即爲一個事件流集合),則所有聚合函數都返回null。


2.Group by

Group by通常配合聚合函數使用。語法和SQL基本一樣,產生的效果就是以某一個或者多個字段進行分組,然後使聚合函數作用於不同組的數據。簡單語法如下:

[plain] view plaincopy
  1. group by aggregate_free_expression [, aggregate_free_expression] [, ...]  
使用Group by要注意一下幾點:

1.Group by後面的內容不能包含聚合函數

2.Group by後面的內容不能是之前select子句中聚合函數修飾的屬性名

3.通常情況要保證分組數量有限制,以防止內存溢出。但是如果分組分了很多,就需要使用@Hint加以控制。


2.1.Group by基本用法

針對上面的第三點,後面再說,先舉幾個例子說明下簡單用法:

[plain] view plaincopy
  1. // 根據color和size來對10個Apple事件進行分組計算平均price  
  2. select avg(price) as aPrice, color, size from Apple.win:length_batch(10) group by color,size  
該句子遵從SQL的標準,如果某個事件的color和size和之前進入的事件的一樣,則歸爲一組,否則新建一組,並計算平均price
[plain] view plaincopy
  1. // 根據size來對10個Apple事件進行分組計算平均price和color  
  2. select avg(price) as aPrice, color, size from Apple.win:length_batch(10) group by size  

可以發現,group by的對象只有size,而select中color不聚合,則生成的結果時,聚合函數會根據相同的size分組進行平均price的計算,但是color不是分組條件,所以color有多少個就有多少組,即使存在一樣的color也不會影響分組數量(實際上就是不分組),但一定記住,聚合函數還是會根據分組條件計算其修飾的屬性。

[plain] view plaincopy
  1. // 根據size來對10個Apple事件進行分組計算平均price和color<pre name="code" class="plain">select avg(price) as aPrice, color from Apple.win:length_batch(10) group by size</pre>  
這一次select子句中沒有包含分組的字段size,但是效果和上一個句子一樣。Esper仍然會根據相同的size進行分組計算平均price,只不過計算結果中只有平均price和color,並且有十排結果。
[plain] view plaincopy
  1. // 根據size乘color來對10個Apple事件進行分組計算平均price<pre name="code" class="plain">select avg(price) as aPrice, size*color from Apple.win:length_batch(10) group by size*color</pre>  
group by的對象只是一個值,以相同的值進行分組,所以上面和和普通的屬性字段一樣,計算一個值進行分組。如果group by後面的表達式值爲null,則所有爲null的事件都被分爲一組進行計算。但是如果使用了count函數,則表達式爲null的事件不會被計算在內。


2.2.@Hint

@Hint是Esper中註解的其中一個,如果不瞭解註解,可以先看看Esper學習之五:EPL語法(一)的第7節再繼續閱讀@Hint的內容。之前對@Hint一筆帶過,那是因爲它是專用於Group by的。我們平時使用Group by的時候,會遇到分組數量太多的情況。比如以時間單位進行分組,那麼內存使用一定是一個大問題。因此@Hint爲其設計了兩個屬性,用於限制Group by的生存時間,使虛擬機能及時回收內存。這兩個屬性分別爲reclaim_group_aged和reclaim_group_freq


reclaim_group_aged

該屬性後面跟着的是正整數,以秒爲單位,表示在n秒內,若分組的數據沒有進行更新,則分組數據被Esper回收。例如:

[plain] view plaincopy
  1. // 根據color對10秒內進入的Apple事件進行分組計算平均price,並且對5秒內沒有數據更新的分組進行回收  
  2. @Hint('reclaim_group_aged=5')select avg(price) as aPrice, color from Apple.win:time(10 sec) group by color //括號內可以使單引號也可以是雙引號  


reclaim_group_freq

該屬性後面跟着的是正整數,以秒爲單位,表示每n秒清理一次分組,可清理的分組是reclaim_group_aged決定的,也就是說要使用該參數,就要配合reclaim_group_aged一起使用。可能不是很好理解,先看看例子:

[plain] view plaincopy
  1. // 根據color對10秒內進入的Apple事件進行分組計算平均price。對8秒內沒有數據更新的分組進行回收,每2秒回收一次  
  2. @Hint('reclaim_group_aged=8,reclaim_group_freq=2')select avg(price) as aPrice, color from Apple.win:time(10 sec) group by color  
        如果不使用reclaim_group_freq屬性,則默認值和reclaim_group_aged的值一樣,對上面來說就是回收的條件爲8秒內沒有數據更新,且每8秒回收一次。這樣的話有可能出現這麼一種情況,上一個8秒的某個分組在下一個8秒還沒到達時就已經持續8秒沒有數據更新了(這句話會不會有點繞?),但是必須等到回收的時間點到達時才能回收這個分組。在分組產生很快的情況下,這樣的回收不及時很可能會造成內存溢出。reclaim_group_freq正是爲這種情況做準備,回收的頻率高一些,在一定程度上能提高內存的使用率。

        上面這兩個屬性的值除了可以使用正整數之外,也可以使用預先定義的變量或者常量


3.Having

Having的用法和SQL一樣,後面跟的是對聚合函數的計算結果進行過濾。Where子句不能包含聚合函數,所以就由Having來完成。示例如下:

[plain] view plaincopy
  1. // 根據size來對10個Apple事件進行分組計算平均price和color,並且排除平均price大於5的分組<pre name="code" class="plain"><pre name="code" class="plain">select avg(price) as aPrice, color from Apple.win:length_batch(10) group by size having avg(price) > 5</pre></pre>  

通常Having配合Group by使用,如果沒有使用Group by,那麼就只有一組。例如:

[plain] view plaincopy
  1. // 根據size來對10個Apple事件計算平均price和color,如果平均price大於5,則數據被排除掉<pre name="code" class="plain"><pre name="code" class="plain">select avg(price) as aPrice, color from Apple.win:length_batch(10) having avg(price) > 5</pre></pre>  
Having後面可以跟多個判斷式子,並且用and,or或者not進行連接。例如:

[plain] view plaincopy
  1. // 根據size來對10個Apple事件計算平均price和color,如果平均price大於5並且平均size小於3,則數據被排除掉<pre name="code" class="plain"><pre name="code" class="plain">select avg(price) as aPrice, color from Apple.win:length_batch(10) having avg(price) > 5 and avg(size) < 3</pre></pre>  

4.Output

4.1.基本語法

Output是EPL中非常有用的東西,用來控制Esper對事件流計算結果的輸出時間和形式,可以以固定頻率,也可以是某個時間點輸出。簡單語法如下:

[plain] view plaincopy
  1. output [after suppression_def]  
  2. [[all | first | last | snapshot] every time_period | output_rate events]  
after suppression_def是可選參數,表示先滿足一定的條件再輸出。

all | first | last | snapshot表明輸出結果的形式,默認值爲all。

every output_rate表示輸出頻率,即每達到規定的頻率就進行輸出。time_period表示時間頻率,相關語法在Esper學習之五:EPL語法(一)的第2節有說到。output_rate events表示事件數量。

舉例說明如下:

[plain] view plaincopy
  1. // 30分鐘內,每進入一個OrderEvent,統計一次sum price,並且每60秒輸出一次統計結果。  
  2. select sum(price) from OrderEvent.win:time(30 min) output snapshot every 60 seconds  


4.2.after

之前在講解Context的時候,有簡單說到過after。關於Context,可參看Esper學習之四:Context。after在output裏的使用也很簡單,語法如下:

[plain] view plaincopy
  1. output after time_period | number events [...]  
time_period表示時間段,number events表示事件數量。表示從EPL可用開始,經過一段時間或者接收到一定數量的事件再進行輸出。例如:

[plain] view plaincopy
  1. // 統計20個Apple事件的sum price,並且在有5個Apple事件進入後纔開始輸出統計結果  
  2. select sum(price) from Apple.win:length(20) output after 5 events  
上面這個句子從第一個進入的事件進行統計,直到進入了5個事件以後才輸出統計結果,之後每進入一個事件輸出一次(這是win:length的特性)。但是要注意的是,after之後的時間長度和事件數量會影響之後的時間或者事件數量。什麼意思?看個完整例子:

[java] view plaincopy
  1. /** 
  2.  *  
  3.  * @author luonanqin 
  4.  * 
  5.  */  
  6. class Banana  
  7. {  
  8.     private int id;  
  9.     private int price;  
  10.   
  11.     public int getId()  
  12.     {  
  13.         return id;  
  14.     }  
  15.   
  16.     public void setId(int id)  
  17.     {  
  18.         this.id = id;  
  19.     }  
  20.   
  21.     public int getPrice()  
  22.     {  
  23.         return price;  
  24.     }  
  25.   
  26.     public void setPrice(int price)  
  27.     {  
  28.         this.price = price;  
  29.     }  
  30.   
  31.     public String toString()  
  32.     {  
  33.         return "id: " + id + ", price: " + price;  
  34.     }  
  35. }  
  36.   
  37. class OutputAfterListener implements UpdateListener  
  38. {  
  39.     public void update(EventBean[] newEvents, EventBean[] oldEvents)  
  40.     {  
  41.         if (newEvents != null)  
  42.         {  
  43.             int price = (Integer) newEvents[0].get("sPrice");  
  44.             System.out.println("Banana's sum price is " + price);  
  45.         }  
  46.     }  
  47. }  
  48.   
  49. public class OutputAfterTest  
  50. {  
  51.     public static void main(String[] args) throws InterruptedException  
  52.     {  
  53.         EPServiceProvider epService = EPServiceProviderManager.getDefaultProvider();  
  54.   
  55.         EPAdministrator admin = epService.getEPAdministrator();  
  56.   
  57.         String banana = Banana.class.getName();  
  58.         // 統計最新3個Banana事件的sum price,並且從EPL可用起,等待第一個事件進入後,以每兩個事件進入的頻率輸出統計結果  
  59.         String epl = "select sum(price) as sPrice from " + banana + ".win:length(3) output after 1 events snapshot every 2 events";  
  60.   
  61.         EPStatement state = admin.createEPL(epl);  
  62.         state.addListener(new OutputAfterListener());  
  63.   
  64.         EPRuntime runtime = epService.getEPRuntime();  
  65.   
  66.         Banana b1 = new Banana();  
  67.         b1.setId(1);  
  68.         b1.setPrice(6);  
  69.         System.out.println("Send Banana Event: " + b1);  
  70.         runtime.sendEvent(b1);  
  71.   
  72.         Banana b2 = new Banana();  
  73.         b2.setId(2);  
  74.         b2.setPrice(3);  
  75.         System.out.println("Send Banana Event: " + b2);  
  76.         runtime.sendEvent(b2);  
  77.   
  78.         Banana b3 = new Banana();  
  79.         b3.setId(3);  
  80.         b3.setPrice(1);  
  81.         System.out.println("Send Banana Event: " + b3);  
  82.         runtime.sendEvent(b3);  
  83.   
  84.         Banana b4 = new Banana();  
  85.         b4.setId(4);  
  86.         b4.setPrice(2);  
  87.         System.out.println("Send Banana Event: " + b4);  
  88.         runtime.sendEvent(b4);  
  89.   
  90.         Banana b5 = new Banana();  
  91.         b5.setId(5);  
  92.         b5.setPrice(4);  
  93.         System.out.println("Send Banana Event: " + b5);  
  94.         runtime.sendEvent(b5);  
  95.     }  
  96. }  
執行結果:

[plain] view plaincopy
  1. Send Banana Event: id: 1, price: 6  
  2. Send Banana Event: id: 2, price: 3  
  3. Send Banana Event: id: 3, price: 1  
  4. Banana's sum price is 10  
  5. Send Banana Event: id: 4, price: 2  
  6. Send Banana Event: id: 5, price: 4  
  7. Banana's sum price is 7  
由此可見,after之後的every子句要等到after後面的表達式滿足後才生效。所以第一個事件進入後,every 2 events生效,即等待兩個事件進入後才輸出結果。對於時間也是要等到after的子句滿足後纔開始計時。例如:

[plain] view plaincopy
  1. // 從EPL可用開始計時,經過1分鐘後,每5秒輸出一次當前100秒內的所有Banana的avg price(即:第一次輸出在65秒時)  
  2. select avg(price) from Banana.win:time(100 sec) after 1 min snapshot every 5 sec  

4.3.first,last,all,snapshot

每當達到輸出時間點時,可以用這四個參數來控制輸出內容。下面分別介紹並舉例。

first

表示每一批可輸出的內容中的第一個事件計算結果。比如:

[plain] view plaincopy
  1. select * from Fruit output first every 2 events  
上面的句子表示每進入兩個Fruit事件,輸出這兩個事件的第一個。
last

和first類似,表示每一批可輸出的內容中的最後一個事件計算結果。比如:

[plain] view plaincopy
  1. select * from Fruit output last every 2 events  
上面的句子表示每進入兩個Fruit事件,輸出這兩個事件的第二個,也就是最後一個。

snapshot

表示輸出EPL所保持的所有事件計算結果,通常用來查看view或者window中現存的事件計算結果。比如:

[plain] view plaincopy
  1. select * from Fruit.win:time(5 sec) output snapshot every 2 events  
上面的句子表示每進入兩個事件輸出5 sec內的所有事件,且不會講這些事件從5 sec範圍內移除

all

也是默認值。和snapshot類似,也是輸出所有的事件,但是不同的是,snapshot相當於對計算結果拍了一張照片,把結果複製出來並輸出,而all是把計算結果直接輸出,不會複製。比如:

[plain] view plaincopy
  1. select * from Fruit.win:time(5 sec) output all every 2 events  
上面的句子表示每進入兩個事件輸出5 sec內包含的所有事件,輸出的事件不再保留於5 sec範圍內。


4.4.Crontab Output

output的另一個語法可以建立定時輸出,關鍵字是at。語法如下:

[plain] view plaincopy
  1. output [after suppression_def]  
  2. [[all | first | last | snapshot] at  
  3. (minutes, hours, days of month, months, days of week [, seconds])]  
minutes, hours, days of month, months, days of week [, seconds]這些都是時間單位,語法後面再細說。舉個簡單的例子:

[plain] view plaincopy
  1. // 在8點到17點這段時間內,每15分鐘輸出一次  
  2. select * from Fruit output at (*/15,8:17,*,*,*)  


4.5.when

Output還可以使用when來實現達到某個固定條件再輸出的效果,一般通過變量,用戶自定義的 函數以及output內置的屬性來實現。基本語法如下:

[plain] view plaincopy
  1. output [after suppression_def]  
  2. [[all | first | last | snapshot] when trigger_expression  
  3. [then set variable_name = assign_expression [, variable_name = assign_expression [,...]]]  
trigger_expression返回true或者false,表示輸出或者不輸出

then set variable_name=assign_expression表示是當trigger_expression被觸發時,可對變量重新賦值。完整例子如下:

[java] view plaincopy
  1. /** 
  2.  *  
  3.  * @author luonanqin 
  4.  * 
  5.  */  
  6. class Pink  
  7. {  
  8.     private int id;  
  9.     private int price;  
  10.   
  11.     public int getId()  
  12.     {  
  13.         return id;  
  14.     }  
  15.   
  16.     public void setId(int id)  
  17.     {  
  18.         this.id = id;  
  19.     }  
  20.   
  21.     public int getPrice()  
  22.     {  
  23.         return price;  
  24.     }  
  25.   
  26.     public void setPrice(int price)  
  27.     {  
  28.         this.price = price;  
  29.     }  
  30.   
  31.     public String toString()  
  32.     {  
  33.         return "id: " + id + ", price: " + price;  
  34.     }  
  35. }  
  36.   
  37. class OutputWhenListener implements UpdateListener  
  38. {  
  39.     public void update(EventBean[] newEvents, EventBean[] oldEvents)  
  40.     {  
  41.         if (newEvents != null)  
  42.         {  
  43.             for (int i = 0; i < newEvents.length; i++)  
  44.             {  
  45.                 Pink pink = (Pink) newEvents[i].getUnderlying();  
  46.                 System.out.println("Output Pink: " + pink);  
  47.             }  
  48.         }  
  49.     }  
  50. }  
  51.   
  52. public class OutputWhenTest  
  53. {  
  54.     public static void main(String[] args) throws InterruptedException  
  55.     {  
  56.         EPServiceProvider epService = EPServiceProviderManager.getDefaultProvider();  
  57.   
  58.         EPAdministrator admin = epService.getEPAdministrator();  
  59.         ConfigurationOperations config = admin.getConfiguration();  
  60.         config.addVariable("exceed"boolean.classfalse);  
  61.   
  62.         String pink = Pink.class.getName();  
  63.         // 當exceed爲true時,輸出所有進入EPL的事件,然後設置exceed爲false  
  64.         String epl = "select * from " + pink + " output when exceed then set exceed=false";  
  65.   
  66.         EPStatement state = admin.createEPL(epl);  
  67.         state.addListener(new OutputWhenListener());  
  68.   
  69.         EPRuntime runtime = epService.getEPRuntime();  
  70.   
  71.         Random r = new Random(47);  
  72.         for (int i = 1; i <= 10; i++)  
  73.         {  
  74.             int price = r.nextInt(10);  
  75.             Pink p = new Pink();  
  76.             p.setId(i);  
  77.             p.setPrice(price);  
  78.             System.out.println("Send Pink Event: " + p);  
  79.             runtime.sendEvent(p);  
  80.             // 當price大於5時,exceed變量爲true  
  81.             if (price > 5)  
  82.             {  
  83.                 runtime.setVariableValue("exceed"true);  
  84.                 // 因爲主線程和輸出線程不是同一個,所以這裏休息1秒保證輸出線程將事件全部輸出,方便演示。  
  85.                 Thread.sleep(1000);  
  86.             }  
  87.         }  
  88.     }  
  89. }  
執行結果:

[plain] view plaincopy
  1. Send Pink Event: id: 1, price: 8  
  2. Output Pink: id: 1, price: 8  
  3. Send Pink Event: id: 2, price: 5  
  4. Send Pink Event: id: 3, price: 3  
  5. Send Pink Event: id: 4, price: 1  
  6. Send Pink Event: id: 5, price: 1  
  7. Send Pink Event: id: 6, price: 9  
  8. Output Pink: id: 2, price: 5  
  9. Output Pink: id: 3, price: 3  
  10. Output Pink: id: 4, price: 1  
  11. Output Pink: id: 5, price: 1  
  12. Output Pink: id: 6, price: 9  
  13. Send Pink Event: id: 7, price: 8  
  14. Output Pink: id: 7, price: 8  
  15. Send Pink Event: id: 8, price: 0  
  16. Send Pink Event: id: 9, price: 2  
  17. Send Pink Event: id: 10, price: 7  
  18. Output Pink: id: 8, price: 0  
  19. Output Pink: id: 9, price: 2  
  20. Output Pink: id: 10, price: 7  
        從結果可以看出來。當price大於5的時候,設置exceed變量爲true,即可輸出之前進入的所有事件,then set子句將exceed設置爲false,等待下一次exceed=true時觸發輸出。由於輸出線程是單獨的線程,所以如果不sleep,結果可能會和這個不同。

對於when關鍵字,Esper提供了一些內置的屬性幫助我們實現更復雜的輸出約束。如圖所示:

以上5個屬性我就不多做解釋了,使用方式是作爲trigger_expression跟在when關鍵字的後面。例如:

[plain] view plaincopy
  1. // 進入的Apple事件總數達到5個時才輸出,且不清零count_insert_total屬性,繼續累加事件總數  
  2. select * from Apple output when count_insert_total=5  
  3.   
  4. // 移除的Apple事件總數達到4個時才輸出,並清零count_remove屬性  
  5. select * from Apple output when count_remove=4  
另外,在使用when的時候,有兩點需要注意:

1.當trigger_expression返回true時,Esper會輸出從上一次輸出之後到這次輸出之間所有的insert stream和remove stream。

2.若trigger_expression不斷被觸發並返回true時,則Esper最短的輸出間隔爲100毫秒。

3.expression不能包含事件流的屬性,聚合函數以及prev函數和prior函數


4.6.Context Terminated

Output還針對Context專門設計了一個輸出條件,即在Context終止時輸出Context中的內容。關於Context,可以看看Esper學習之四:Context。具體語法如下:

[plain] view plaincopy
  1. output when terminated [and termination_expression]  
  2. [then set variable_name = assign_expression [, variable_name = assign_expression [,...]]]]  
when terminated是關鍵字,之前可以通過and連接其他的式子一起使用。termination_expression是一個返回true或者false的表達式,同trigger_expression一樣。舉例如下:

[plain] view plaincopy
  1. // 在MyContext下,查詢context的id並計算Apple的sum price,當Context結束且輸入的事件總數大於10時,輸出。然後設置FinishCompute變量爲true  
  2. context MyContext select context.id, sum(price) from Apple output when terminated and count_insert_total > 10 then set FinishCompute = true  
  3.   
  4. // 在MyContext下,計算Apple的avg size,並每1分鐘輸出第一個進入的事件計算結果,當context結束時也輸出一次計算結果  
  5. context MyContext select avg(size) from Apple output first every 1 min and when terminated  


        Output和Aggregation,Group by一起使用時,first,last,all,snapshot四個關鍵字產生的效果會比較特別。建議各位自己看看Esper的官方文檔的Appendix A,有相當完整的例子做說明,因爲篇幅較長,所以我沒有放在文章裏進行講解。另外針對first,last,all,snapshot四個關鍵字,只有使用snapshot是不會緩存計算結果。其他的關鍵字會緩存事件直到觸發了輸出條件纔會釋放,所以如果輸入的數據量比較大,就要注意輸出條件被觸發前的內存使用量。


        關於Output的內容比較多,使用起來也比較靈活。各位在使用的時候,也許會發現自己寫的達不到預期的效果,本人在使用的時候也遇到過,所以還請各位耐心地多試幾次。Group by和Aggregation和SQL的類似,所以使用起來很容易。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章