淺解比SQL更好用的SPL
從 SQL 到集算器的基本查詢語法遷移(一)單表操作
資料庫和資料分析領域,有一個強大的存在,大名 SQL,全名結構化查詢語言 (Structured Query Language)。從字面上看,這個語言的目標就是把資料“查詢”出來,而查詢這個詞給人的感覺並不是很難。但實際上,為了支援貌似簡單的查詢,需要進行大量的計算動作,甚至整個過程就是一個多步驟的計算,前後步驟之間還有很強的依賴關係,前面計算的結果要被後面使用,而後面的輸出有可能需要我們對前面的計算進行調整。
打個比方,這有點類似於去各類網點辦事,填表遞交後,相關辦事人員開始在視窗後忙忙碌碌,時不時還會甩回來幾個問題要求澄清,等到最後拿到回覆,還有可能根本不是自己期望的結果!這時候,坐在辦事視窗外的我們,抓狂之餘,往往會產生一個念頭,如果我們能夠看到,甚至參與到過程中,應該能夠大大地提高辦事效率。
沒錯,你應該能想到,下面要介紹的集算器,和 SQL 相比對於我們這些過程控來說,就是一個可以輕鬆把玩和控制的計算(不止是查詢)工具。
我們要做的,就是“照貓畫虎”地把習慣中的 SQL 操作遷移到集算器中,用小小的習慣改變,換來大大的效益提升。
首先,我們需要把資料從傳統的資料來源中“搬遷”到集算器中,這樣後續的操作就可以完全在集算器中進行了。
我們最常用的資料來源一般就是關係資料庫 RDB。這裡使用的樣例資料,就是資料庫中的兩個資料表:
訂單資訊表(order,主鍵 orderId),包括訂單編號orderId、客戶程式碼customerId、僱員編號employeeId、訂單日期orderDate
orderId | customerId | employeeId | orderDate | sendDate | money |
---|---|---|---|---|---|
10248 | VINET | 2 | 2011-02-04 | 2011-02-16 | 440 |
10249 | TOMSP | 9 | 2011-02-05 | 2011-02-10 | 1863 |
10250 | HANAR | 9 | 2011-02-08 | 2011-02-12 | 1813 |
10251 | VICTE | 9 | 2011-02-08 | 2011-02-15 | 670 |
訂單明細表(orderDetail,主鍵 orderId,productId),包括訂單編號orderId、產品編號productId、價格price、數量amount、折扣discount:
orderId | productId | price | amount | discount |
---|---|---|---|---|
11059 | 17 | 39 | 12 | 0.85 |
11059 | 60 | 34 | 35 | 0.9 |
11060 | 60 | 34 | 4 | 1 |
11060 | 77 | 13 | 10 | 0.95 |
“搬遷”,或者說匯入資料的過程非常簡單,如下圖所示:
集算器 | A |
---|---|
1 | =connect(“hsqlDB”) |
2 | =A1.query(“select * from order”) |
3 | >A1.close() |
首先建立資料庫連線(網格 A1),然後直接通過單表全量查詢的 SQL 語句從資料庫中讀取資料(網格 A2),最後清理現場,關閉資料庫連線(網格 A3)。
在執行了指令碼後,我們可以選中網格 A2,在結果區中看看搬過來的資料,同時,order 表在集算器中也換了個身份,我們稱之為“序表”,用網格名稱 A2 代表。序表是集算器中一個非常重要的概念,現在我們可以簡單地把它理解成對應資料庫中的一張表:
其實,在集算器中,任何一個有計算結果的網格(一般是用等號“=”開始),都可以在執行指令碼後,隨時選中並檢視結果,並通過網格名稱 A7、B8 等隨時引用,從而滿足我們隨時監控的慾望……
接下來,我們以 SQL 中 select 語句的各個子句為線索,看看集算器中是如何操作的:
第一個:SELECT 子句
用來選擇需要查詢的欄位,也可以通過表示式來對欄位進行計算,或者重新命名欄位。與之對應的,集算器裡有 new、derive、run 三個函式。
例如:只選出訂單表裡的訂單編號、僱員編號、訂單日期以及金額欄位,同時把金額乘以 100 後使它的單位從元變成分,把相應的結果欄位重新命名為 centMoney。
SQL 的做法如下:
SQL |
---|
SELECT orderId,employeeId,orderDate,money*100 centMoney FROM order |
集算器對應的做法是下表中的 A3:
集算器 | A |
---|---|
1 | =connect(“hsqlDB”) |
2 | =A1.query(“SELECT * FROM order”) |
3 | =A2.new(orderId,employeeId,orderDate,money*100:centMoney) |
4 | =A3.derive(year(orderDate):orderYear) |
5 | =A4.run(string(orderYear)+”年”:orderYear) |
6 | >A1.close() |
A3 利用 A2 的資料新建了一個序表,包含了要求的欄位,包括把金額乘以 100 後用 centMoney 命名:
我們繼續看下 A4 的結果,在序表 A3 的原有欄位後增加了新欄位 orderYear,這也就是說 derive(新計算欄位) 相當於 new(所有老欄位, 新計算欄位),可以看做是 new 函式的一種簡單的寫法,免去了把老欄位再抄寫一遍。
A5 使用了 run 函式,直接作用是修改老欄位 orderYear 的值,但是再看 A4 的結果,也變成和 A5 一樣了。這是因為 run 函式並沒有像 new、derive 函式那樣生成新的序表物件,而是在原來物件上直接做修改。
總結一下,在集算器中,new、derive、run 函式都會產生序表結果,但是 new、derive 函式會生成一個新的序表物件,像是把資料複製出來(這個行為有個專有名詞immutable),而 run 則直接修改被處理物件(行為屬於mutable)。
【延伸閱讀】之所以提出mutable這樣的行為,有兩個因素:首先是減少記憶體佔用,從而提高效能;其次,有些實際業務的需求就需要改變原有物件,一系列的動作直接作用在一個物件上,到最後一步就得到正確結果了,而不是得到一堆中間結果,再做最後合併的動作。當然 immutable 也有它的適用場景,兩種行為本身沒有優劣之分。
第二個:WHERE 子句
用來對資料表按條件進行過濾。與之對應的,集算器通過 select 函式對一個序表的記錄進行過濾。效果如下圖所示:
針對前面的示例資料,我們希望查詢指定時段(2012 年 1 月期間)的訂單資料,可以對比一下 SQL 和集算器(網格 A3)的做法。
SQL |
---|
SELECT * FROM order |
WHERE orderDate>=’2012-01-01′ AND orderDate<‘2012-02-01’ |
集算器 | A |
---|---|
1 | =connect(“hsqlDB”) |
2 | =A1.query(“SELECT * FROM order”) |
3 | =A2.select(orderDate>=date(“2012-01-01”) && orderDate<date(“2012-02-01”)) |
4 | >A1.close() |
需要注意一下集算器表示式中有兩個細節:一是用了 date 函式把字串轉換成日期型別,二是 AND/OR 在集算器裡的寫法是 &&/||。
A3 的結果如下:
看起來和 A2 結構一致,只是資料量小了。但是我們可以做一個實驗,在網格 B3 中輸入“=A2.select(orderId=10555).run(money*10:money)”,修改 A2 序表中某些記錄的欄位值,可以看到 A3 序表裡這些對應記錄的值也跟著變化了。這就說明兩個序表裡的記錄就是同一個東西(叫做物件會顯得更專業點),也就是說集算器裡的 select 函式屬於我們前面所說的 mutable 行為。
第三個:GROUP BY 子句
GROUPY BY 經常和聚合函式 SUM、COUNT 等一起出現,用來將查詢結果按照某些欄位進行歸類分組,並彙總統計。嚴格來說,這是兩個獨立的動作,但在 SQL 中總是一起出現,從而給人一種兩者必須同時使用的假象。事實上,這種組合是對分組操作的一種侷限,或者說分組之後,能夠進行的計算遠不止 SQL 中的幾種聚合函式。在集算器中,與 GROUP BY 子句對應的是 group 函式,它可以只對資料純粹分組,每組形成一個小集合,在後面的計算中可以針對這些小集合進行簡單的聚合,也可以進行更加複雜的計算。下圖是 SQL 中利用 GROUP BY 進行分組求和的示意:
同樣還是前面的示例資料,我們希望計算 2012 年 1 月期間每個僱員的銷售總額,也就是按照僱員編號分組後求和。針對這個分組求和的計算,我們對比一下 SQL 和集算器的做法:
SQL |
---|
SELECT employeeId, sum(money) salesAmount FROM order WHERE orderDate>=’2012-01-01′ AND orderDate<‘2012-02-01’ GROUP BY employeeId |
集算器 | A |
---|---|
1 | =connect(“hsqlDB”) |
2 | =A1.query(“SELECT * FROM order”) |
3 | =A2.select(orderDate>=date(“2012-01-01”) && orderDate<date(“2012-02-01”)) |
4 | =A3.group(employeeId;~.sum(money):salesAmount) |
5 | >A1.close() |
A4 的結果如下:
集算器把查詢分解成了三步:
首先,是 A2 取出訂單表中的所有記錄;
然後,A3 過濾得到指定時段(2012 年 1 月期間)的訂單記錄
最後,A4 把過濾得到的記錄按照僱員編號進行分組(由函式引數中分號前的部分定義,可以同時指定多個欄位),同時對每個分組(用“~”符號表示)進行求和(sum)計算,並把計算結果的欄位命名為 salesAmount(分號後的部分)
看起來和 SQL 分組沒什麼不用,這只是因為我們這個例子只演示了和 SQL 相同的分組查詢。實際上 A4 裡 group 函式的後半部分不是必須的,也可能有的業務僅僅是想得到分組後的結果,而不在這裡進行求和、計數運算;也可能針對特定值的分組有不同的聚合運算,那就針對分組後的小集合“~”寫更復雜的表示式就可以了。
同時,在其他教程中,我們還可以瞭解到,分組欄位不侷限於原始欄位,可以是一個表示式,這個和 SQL 裡一樣。
單純的分組屬於mutable行為,是把一個大集合的記錄拆分到多個小集合裡。而加上聚合運算後,因為產生了新的資料結構,就不再是簡單的把原有記錄挪挪地方的操作了。
第四個:HAVING 子句
用來對 GROUP BY 後的聚合結果再進行過濾。在集算器裡,就沒有專門對應 HAVING 的函數了,對任何序表按照條件過濾都用 select 函式,因為計算是分步的,操作指向的物件總是明確的。而 SQL 要求一句話把所有事情表達出來,遇到複雜查詢,就難免出現對同一性質的操作增加概念,以表達作用到不同的物件上。再深想一下,HAVING 概念在 SQL 裡也不是必須的,它是對第一層子查詢的簡化寫法:
SELECT f1, sum(f2) f2 FROM t GROUP BY f1 HAVING sum(f2)>100
等價於
SELECT * FROM
(SELECT f1, sum(f2) f2sum FROM t GROUP BY f1) t2
WHERE f2sum >100
對更多層子查詢做同類簡化,估計會出現 HAVING2、HAVING3…類似的關鍵字,但 HAVING2 之後的簡化價效比不高,SQL 也就沒有提供了。這裡又體現出分步計算的一個優勢,只需要描述計算本質需要的概念,HAVING、子查詢這些因為技術手段被迫產生的概念就可以棄用了。減少非必要概念是降低學習成本的重要手段。
我們具體看一下 SQL 和集算器的做法的對比,找到 2012 年 1 月期間銷售額超過 5000 的僱員編號和他的銷售總額:
SQL |
---|
SELECT employeeId, SUM(money) salesAmount FROM order WHERE orderDate>=’2012-01-01′ AND orderDate<‘2012-02-01’ GROUP BY employeeId HAVING SUM(money)>5000 |
集算器 | A |
---|---|
1 | =connect(“hsqlDB”) |
2 | =A1.query(“SELECT * FROM order”) |
3 | =A2.select(orderDate>=date(“2012-01-01”) && orderDate<date(“2012-02-01”)) |
4 | =A3.group(employeeId;~.sum(money):salesAmount) |
5 | =A4.select(salesAmount>5000) |
6 | >A1.close() |
A5 結果
隨著查詢複雜度逐步提升,集算器語句容易閱讀,步驟流程清晰的特點也就凸顯出來了。每一步都可以觀察結果,根據結果隨意控制計算流程,用最精簡的概念描述每個計算步驟。這還只是一個最簡單的單表查詢例子,下一篇我們會繼續瞭解在多表連線和聯合的情況下,集算器會有怎樣更加優秀的表現。