上篇說到了Select Clause和From Clause,今天這篇就說說Aggregation,Group by,Having和Output Clause。先預告一下,由於例子比較多,所以篇幅會有些長,需要各位耐心觀看哦。
1.Aggregation
和SQL一樣,EPL也有Aggregation,即聚合函數。語法如下:
- aggregate_function([all|distinct] expression)
- // 查詢最新5秒的Apple的平均價格
- select avg(price) as aPrice from Apple.win:time(5 sec)
- // 查詢最新10個Apple的價格總和的兩倍
- select sum(price*2) as sPrice from Apple.win:length(10)
- // 查詢最新10個Apple的價格,並用函數計算後再算平均值
- select avg(Compute.getResult(price)) from Apple.win:length(10)
可以使用distinct關鍵字對expression加以約束,表示去掉expression產生的重複的值。默認情況下爲all關鍵字,即所有的expression值都參與聚合運算。例如:
- // 查詢最新5秒的Apple的平均價格
- select avg(distinct price) as aPrice from Apple.win:time(5 sec)
- // 假如: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基本一樣,產生的效果就是以某一個或者多個字段進行分組,然後使聚合函數作用於不同組的數據。簡單語法如下:
- group by aggregate_free_expression [, aggregate_free_expression] [, ...]
1.Group by後面的內容不能包含聚合函數
2.Group by後面的內容不能是之前select子句中聚合函數修飾的屬性名
3.通常情況要保證分組數量有限制,以防止內存溢出。但是如果分組分了很多,就需要使用@Hint加以控制。
2.1.Group by基本用法
針對上面的第三點,後面再說,先舉幾個例子說明下簡單用法:
- // 根據color和size來對10個Apple事件進行分組計算平均price
- select avg(price) as aPrice, color, size from Apple.win:length_batch(10) group by color,size
- // 根據size來對10個Apple事件進行分組計算平均price和color
- 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也不會影響分組數量(實際上就是不分組),但一定記住,聚合函數還是會根據分組條件計算其修飾的屬性。
- // 根據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>
- // 根據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>
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回收。例如:
- // 根據color對10秒內進入的Apple事件進行分組計算平均price,並且對5秒內沒有數據更新的分組進行回收
- @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一起使用。可能不是很好理解,先看看例子:
- // 根據color對10秒內進入的Apple事件進行分組計算平均price。對8秒內沒有數據更新的分組進行回收,每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
上面這兩個屬性的值除了可以使用正整數之外,也可以使用預先定義的變量或者常量
3.Having
Having的用法和SQL一樣,後面跟的是對聚合函數的計算結果進行過濾。Where子句不能包含聚合函數,所以就由Having來完成。示例如下:
- // 根據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,那麼就只有一組。例如:
- // 根據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>
- // 根據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對事件流計算結果的輸出時間和形式,可以以固定頻率,也可以是某個時間點輸出。簡單語法如下:
- output [after suppression_def]
- [[all | first | last | snapshot] every time_period | output_rate events]
all | first | last | snapshot表明輸出結果的形式,默認值爲all。
every output_rate表示輸出頻率,即每達到規定的頻率就進行輸出。time_period表示時間頻率,相關語法在Esper學習之五:EPL語法(一)的第2節有說到。output_rate events表示事件數量。
舉例說明如下:
- // 30分鐘內,每進入一個OrderEvent,統計一次sum price,並且每60秒輸出一次統計結果。
- select sum(price) from OrderEvent.win:time(30 min) output snapshot every 60 seconds
4.2.after
之前在講解Context的時候,有簡單說到過after。關於Context,可參看Esper學習之四:Context。after在output裏的使用也很簡單,語法如下:
- output after time_period | number events [...]
- // 統計20個Apple事件的sum price,並且在有5個Apple事件進入後纔開始輸出統計結果
- select sum(price) from Apple.win:length(20) output after 5 events
- /**
- *
- * @author luonanqin
- *
- */
- class Banana
- {
- private int id;
- private int price;
- public int getId()
- {
- return id;
- }
- public void setId(int id)
- {
- this.id = id;
- }
- public int getPrice()
- {
- return price;
- }
- public void setPrice(int price)
- {
- this.price = price;
- }
- public String toString()
- {
- return "id: " + id + ", price: " + price;
- }
- }
- class OutputAfterListener implements UpdateListener
- {
- public void update(EventBean[] newEvents, EventBean[] oldEvents)
- {
- if (newEvents != null)
- {
- int price = (Integer) newEvents[0].get("sPrice");
- System.out.println("Banana's sum price is " + price);
- }
- }
- }
- public class OutputAfterTest
- {
- public static void main(String[] args) throws InterruptedException
- {
- EPServiceProvider epService = EPServiceProviderManager.getDefaultProvider();
- EPAdministrator admin = epService.getEPAdministrator();
- String banana = Banana.class.getName();
- // 統計最新3個Banana事件的sum price,並且從EPL可用起,等待第一個事件進入後,以每兩個事件進入的頻率輸出統計結果
- String epl = "select sum(price) as sPrice from " + banana + ".win:length(3) output after 1 events snapshot every 2 events";
- EPStatement state = admin.createEPL(epl);
- state.addListener(new OutputAfterListener());
- EPRuntime runtime = epService.getEPRuntime();
- Banana b1 = new Banana();
- b1.setId(1);
- b1.setPrice(6);
- System.out.println("Send Banana Event: " + b1);
- runtime.sendEvent(b1);
- Banana b2 = new Banana();
- b2.setId(2);
- b2.setPrice(3);
- System.out.println("Send Banana Event: " + b2);
- runtime.sendEvent(b2);
- Banana b3 = new Banana();
- b3.setId(3);
- b3.setPrice(1);
- System.out.println("Send Banana Event: " + b3);
- runtime.sendEvent(b3);
- Banana b4 = new Banana();
- b4.setId(4);
- b4.setPrice(2);
- System.out.println("Send Banana Event: " + b4);
- runtime.sendEvent(b4);
- Banana b5 = new Banana();
- b5.setId(5);
- b5.setPrice(4);
- System.out.println("Send Banana Event: " + b5);
- runtime.sendEvent(b5);
- }
- }
- Send Banana Event: id: 1, price: 6
- Send Banana Event: id: 2, price: 3
- Send Banana Event: id: 3, price: 1
- Banana's sum price is 10
- Send Banana Event: id: 4, price: 2
- Send Banana Event: id: 5, price: 4
- Banana's sum price is 7
- // 從EPL可用開始計時,經過1分鐘後,每5秒輸出一次當前100秒內的所有Banana的avg price(即:第一次輸出在65秒時)
- select avg(price) from Banana.win:time(100 sec) after 1 min snapshot every 5 sec
4.3.first,last,all,snapshot
每當達到輸出時間點時,可以用這四個參數來控制輸出內容。下面分別介紹並舉例。
first
表示每一批可輸出的內容中的第一個事件計算結果。比如:
- select * from Fruit output first every 2 events
last
和first類似,表示每一批可輸出的內容中的最後一個事件計算結果。比如:
- select * from Fruit output last every 2 events
snapshot
表示輸出EPL所保持的所有事件計算結果,通常用來查看view或者window中現存的事件計算結果。比如:
- select * from Fruit.win:time(5 sec) output snapshot every 2 events
all
也是默認值。和snapshot類似,也是輸出所有的事件,但是不同的是,snapshot相當於對計算結果拍了一張照片,把結果複製出來並輸出,而all是把計算結果直接輸出,不會複製。比如:
- select * from Fruit.win:time(5 sec) output all every 2 events
4.4.Crontab Output
output的另一個語法可以建立定時輸出,關鍵字是at。語法如下:
- output [after suppression_def]
- [[all | first | last | snapshot] at
- (minutes, hours, days of month, months, days of week [, seconds])]
- // 在8點到17點這段時間內,每15分鐘輸出一次
- select * from Fruit output at (*/15,8:17,*,*,*)
4.5.when
Output還可以使用when來實現達到某個固定條件再輸出的效果,一般通過變量,用戶自定義的 函數以及output內置的屬性來實現。基本語法如下:
- output [after suppression_def]
- [[all | first | last | snapshot] when trigger_expression
- [then set variable_name = assign_expression [, variable_name = assign_expression [,...]]]
then set variable_name=assign_expression表示是當trigger_expression被觸發時,可對變量重新賦值。完整例子如下:
- /**
- *
- * @author luonanqin
- *
- */
- class Pink
- {
- private int id;
- private int price;
- public int getId()
- {
- return id;
- }
- public void setId(int id)
- {
- this.id = id;
- }
- public int getPrice()
- {
- return price;
- }
- public void setPrice(int price)
- {
- this.price = price;
- }
- public String toString()
- {
- return "id: " + id + ", price: " + price;
- }
- }
- class OutputWhenListener implements UpdateListener
- {
- public void update(EventBean[] newEvents, EventBean[] oldEvents)
- {
- if (newEvents != null)
- {
- for (int i = 0; i < newEvents.length; i++)
- {
- Pink pink = (Pink) newEvents[i].getUnderlying();
- System.out.println("Output Pink: " + pink);
- }
- }
- }
- }
- public class OutputWhenTest
- {
- public static void main(String[] args) throws InterruptedException
- {
- EPServiceProvider epService = EPServiceProviderManager.getDefaultProvider();
- EPAdministrator admin = epService.getEPAdministrator();
- ConfigurationOperations config = admin.getConfiguration();
- config.addVariable("exceed", boolean.class, false);
- String pink = Pink.class.getName();
- // 當exceed爲true時,輸出所有進入EPL的事件,然後設置exceed爲false
- String epl = "select * from " + pink + " output when exceed then set exceed=false";
- EPStatement state = admin.createEPL(epl);
- state.addListener(new OutputWhenListener());
- EPRuntime runtime = epService.getEPRuntime();
- Random r = new Random(47);
- for (int i = 1; i <= 10; i++)
- {
- int price = r.nextInt(10);
- Pink p = new Pink();
- p.setId(i);
- p.setPrice(price);
- System.out.println("Send Pink Event: " + p);
- runtime.sendEvent(p);
- // 當price大於5時,exceed變量爲true
- if (price > 5)
- {
- runtime.setVariableValue("exceed", true);
- // 因爲主線程和輸出線程不是同一個,所以這裏休息1秒保證輸出線程將事件全部輸出,方便演示。
- Thread.sleep(1000);
- }
- }
- }
- }
- Send Pink Event: id: 1, price: 8
- Output Pink: id: 1, price: 8
- Send Pink Event: id: 2, price: 5
- Send Pink Event: id: 3, price: 3
- Send Pink Event: id: 4, price: 1
- Send Pink Event: id: 5, price: 1
- Send Pink Event: id: 6, price: 9
- Output Pink: id: 2, price: 5
- Output Pink: id: 3, price: 3
- Output Pink: id: 4, price: 1
- Output Pink: id: 5, price: 1
- Output Pink: id: 6, price: 9
- Send Pink Event: id: 7, price: 8
- Output Pink: id: 7, price: 8
- Send Pink Event: id: 8, price: 0
- Send Pink Event: id: 9, price: 2
- Send Pink Event: id: 10, price: 7
- Output Pink: id: 8, price: 0
- Output Pink: id: 9, price: 2
- Output Pink: id: 10, price: 7
對於when關鍵字,Esper提供了一些內置的屬性幫助我們實現更復雜的輸出約束。如圖所示:
以上5個屬性我就不多做解釋了,使用方式是作爲trigger_expression跟在when關鍵字的後面。例如:
- // 進入的Apple事件總數達到5個時才輸出,且不清零count_insert_total屬性,繼續累加事件總數
- select * from Apple output when count_insert_total=5
- // 移除的Apple事件總數達到4個時才輸出,並清零count_remove屬性
- select * from Apple output when count_remove=4
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。具體語法如下:
- output when terminated [and termination_expression]
- [then set variable_name = assign_expression [, variable_name = assign_expression [,...]]]]
- // 在MyContext下,查詢context的id並計算Apple的sum price,當Context結束且輸入的事件總數大於10時,輸出。然後設置FinishCompute變量爲true
- context MyContext select context.id, sum(price) from Apple output when terminated and count_insert_total > 10 then set FinishCompute = true
- // 在MyContext下,計算Apple的avg size,並每1分鐘輸出第一個進入的事件計算結果,當context結束時也輸出一次計算結果
- 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的類似,所以使用起來很容易。