首頁
>
資源
>
會議活動

2023 IoTDB Summit:天謀科技高級開發工程師田原《盡其用:如何用 IoTDB 發掘數據的無限潛能》

12 月 3 日,2023 IoTDB 用戶大會在北京成功舉行,收獲強烈反響。本次峰會匯集了超 20 位大咖嘉賓帶來工業互聯網行業、技術、應用方向的精彩議題,多位學術泰斗、企業代表、開發者,深度分享了工業物聯網時序數據庫 IoTDB 的技術創新、應用效果,與各行業標桿用戶的落地實踐、解決方案,并共同探討時序數據管理領域的行業趨勢。

我們邀請到天謀科技高級開發工程師,Apache IoTDB PMC Member 田原參加此次大會,并做主題報告——《盡其用:如何用 IoTDB 發掘數據的無限潛能》。以下為內容全文。

大家好,我是來自天謀科技的田原,在天謀科技主要負責 IoTDB 的查詢引擎的相關工作。今天 talk 的題目剛才主持人也介紹了,主要也是跟數據查詢相關的。

我今天的演講主要分這四個部分去展開。先跟大家簡單介紹一下 IoTDB 強大的查詢性能,然后就是 IoTDB 一些具有時序特色的查詢的算子。第三個部分會跟大家介紹一下 UDF 函數庫,在早上的時候王老師可能也提過了,我們清華團隊,其實宋老師組去做了不只有壓縮的算法,還有一些工業上的數據質量的函數庫,都在我們 UDF 函數庫里面會去介紹。第四,會跟大家介紹一些真實的時序場景,并且會跟關系型的 SQL 去做一些對比,大家能夠真實地去感受到,用 IoTDB 的 SQL 去寫一些查詢語句,會比關系型數據庫更加容易。

01 IoTDB 強大的查詢性能

首先,第一個部分就是 IoTDB 強大的查詢性能。程序員可能經常會說的就是:“Talk is cheap, show me the code.” 所以在數據庫領域,大家經常也會說:“talk is cheap, show me the benchmark result.” 在時序場景里面,大家可能比較公認的一個測試套件叫 TSBS,那么 IoTDB 在今年也是上榜了 benchANT。benchANT 是位于德國的一家第三方的,做云設施或者說數據庫測評的一個機構,作為第三方的一個機構,它當然不僅僅是做時序數據庫。它在時序數據庫的這個板塊,主要采用的就是剛剛說的 TSBS 這樣一個測試套件。它包含兩種環境,都是大家在 AWS 上面可復現的,一種是 2 核 8GB 的,還有一種是 4 核 16GB 的這兩種環境。

2023大會田原圖1-20240109.png

IoTDB 在這兩種環境下面表現都相當優異,不能說全面碾壓,只能說遙遙領先。大家可以看到,在 small 環境,就是剛剛提到的 4 核 16GB 的這個環境下面,因為查詢性能分兩方面,第一方面是 QPS,這里列出它每秒能達到 11497 次的查詢操作,比 VictoriaMetrics 高出了 36%,是 InfluxDB 的 5 倍多,是 QuestDB 的 16 倍多。

2023大會田原圖2-20240109.png

在我們的查詢延遲方面,查詢延遲也只有 2 到 3 毫秒,也是遠低于同類的數據庫,大家也可以看到。

2023大會田原圖3-20240109.png

下面是 xsmall 環境,也就是剛剛提到的 2 核 8GB 的這樣一個環境,其實都跟剛剛類似,IoTDB 也是排名第一,比同類的數據庫也高出不少倍。

2023大會田原圖4-20240109.png

包括它的查詢延遲。因為這是一個第三方的機構,大家也可以通過下面這個網址去訪問的,在網上也可以看到。

2023大會田原圖5-20240109.png

這個比較好的測試結果其實是得益于 IoTDB 強大的查詢引擎,它是基于 MPP 的。一條 SQL 過來之后,在 IoTDB 的節點里面,查詢的節點角色分為兩種:第一種是 Coordinator,它負責去接收用戶的 SQL,根據這個 SQL 涉及的數據,然后根據數據分區,把它切分成不同的查詢分片,再把它的查詢分片發放到不同的 Worker 節點,這是另一個角色,發到 Worker 節點去做真正的執行。

當你有多副本的時候,這樣一個架構可以幫你達到一個高可用的效果,因為有節點宕機或者網絡分區的時候,我們可以選擇把查詢分片路由到它的另外一個分片上,也可以根據我們的節點負載去做實時的負載均衡。并且,這個 MPP 天然就可以把查詢發到各個節點上去運行,所以它能夠充分利用這種多機多核的特性。

2023大會田原圖6-20240109.png

剛剛提到的是查詢的切分過程,一個查詢被切分之后,剛剛提到,一個單個的查詢的分片會被發到一個 Worker 節點上去做執行。那這樣一個 Worker 節點上在執行的時候,它還會再進一步地去做切分,進一步地去利用單機多核的特性。

這個切分的時候分兩個方面,一方面可能這個算子數還是很龐大,那我還是要在這個算子數上再進行一次切分。切分完了之后,它可能每一個分片,這里的分片是指的 Pipeline,就是已經到單個 Worker 上面了。那每一個 Pipeline 可能涉及很多個數據文件,那很多個數據文件,我們還能把數據文件再分組,這些分組就對應了多個 Driver,每個 Driver 就是真正可以調度的一些 Task,所以能夠進一步降低我們的查詢延遲。

2023大會田原圖7-20240109.png

有了這個查詢的分片任務之后,那就需要去調度了。IoTDB 設計查詢調度的目標主要有三個:第一個就是因為不同的查詢任務可能有不同的優先級,短任務可能有更高的優先級,因為是實時的分析任務,還有一些批量的歷史數據查詢,它可能涉及的時間范圍比較廣,它的查詢的時間會比較長,那它優先級可能稍微低一些沒有關系。當然,我們的長查詢不能長時間地給饑餓掉,因為它的實時查詢可能不斷地再來,我們要保證我們的歷史分析查詢也能夠正確地去完成。第三點就是,引入一個新的調度器之后,你要確保調度器的開銷就足夠小。

2023大會田原圖8-20240109.png

IoTDB 去做設計的時候也是固定了查詢的線程數,可能各個數據庫都會這么去做,也定制了自己的調度算法,并且也做了一些內存控制,能夠去控制查詢任務的總內存空間,當超過內存閾值的時候就進行服務降級,去避免突發的查詢導致的 OOM。還有就是查詢超時,就是一個長的查詢任務,可能是一些業務人員不小心去誤觸發的,那這樣的一個查詢任務在達到查詢超時時間之后,也會被自動的終止掉,去避免這樣一個長查詢一直占著我們服務器的資源。

2023大會田原圖9-20240109.png

剛剛提到我們定制的,或者說自己去實現的一個調度算法叫多級反饋隊列,這個其實也是操作系統里面比較經典的一個調度算法。它能夠做到我們剛剛說的幾點:第一點就是它的短查詢會擁有更高的優先級,因為所有的任務進來的時候,都是被認為它的優先級是最高的。如果你在我給你的時間分片內執行完,那它其實就是一個短任務,永遠都在第一級隊列里面就執行完了。而當長查詢進來的時候,第一個隊列的時間分片可能用完之后還沒有結束,那我就會把你放到第二級隊列里面,那第二級隊列里面意味著它的優先級會比較低,調度器盡量會去調度 Level 更低的隊列里面的任務。

隨著長查詢慢慢會被放到最后一級 Level 的隊列里面,那我們怎么保證它不會被饑餓呢?因為如果第一級隊列一直有實時任務進來的話,其實這個 MLFQ 算法會把所有還存在的這些 Task,定期地去提高到 Level 0,這樣的話就能夠達到長查詢不會被饑餓的效果。

2023大會田原圖10-20240109.png

02 具有時序意義的算子

跟大家簡單介紹過查詢大的框架之后,跟大家去具體的介紹一下有一些時序語義的特色的算子。時序查詢能力分類主要分為這幾種:第一種,也是比較常見的,就是我們的降采樣查詢,就是把一段時間的數據的趨勢去做出來就行了,因為我們可能并不需要每一個具體的點,每個具體的點畫出來的時候,它可能非常不直觀。上面這張圖,大家也可以看到,如果能夠通過降采樣,其實只要反映它一定的趨勢就行了。

第二點其實就是我們的分段算法,時間序列去做分段。它跟關系型數據庫里面的 GROUP BY 很像,但是關系性數據庫里面它大部分都是 GROUP BY 某一列,只是根據值去做這個分列,它并沒有去做分組,并沒有一個順序的語義,也不會有任何除了值分組之外的一些操作,當然它會有一些 Window Function ,那后面我也會提到。

第三點就是在時序場景里面也會非常常見的,就是我們的空值補齊查詢。

2023大會田原圖11-20240109.png

這個就是剛剛提到的,在 IoTDB 里面會去通過 GROUP BY TIME 這樣一個時間分組的功能,去實現我們的聚合降采樣的功能。這個可能大家會比較熟悉,它有三個參數,當然第三個參數是一個 Optional 可選的。第一個參數就是指定我們查詢的整個時間窗口,第二個參數是指定我們聚合的,也就是降采樣的窗口大小。

2023大會田原圖12-20240109.png

通過一個例子大家可以看到,這個例子是去做了 11 月 1 號到 11 月 7 號這七天,每一天我需要按天去做降采樣,就是這么簡單的一個 SQL,可以看到它會得出來每一天的最大值,還有它的每一天的行數。

2023大會田原圖13-20240109.png

剛剛是沒有用到第三個參數,那如果用到第三個參數,它能做到什么呢?它能夠做到我還是按每一天去對這個數據做降采樣,但是我可能并不需要這一整天的數據,我可能只需要每天凌晨 0 點到凌晨 3 點的數據,那就可以通過第三個參數,這樣一個滑動步長的方式去解決。

2023大會田原圖14-20240109.png

除了剛剛說到的,也有一個大家可能不太會注意到的方式。我們的 GROUP BY TIME 其實是支持兩種方式的,是通過左開右閉,和左閉右開的這種方式去做到。大家可能用第一種方式,左閉右開比較多,左閉右開的話,它可能包含 0,但不包含 9,當有 0、1、2、3、4、5、6、7、8 這九個點的時候,它做完分組之后,第一個分組包含 0、1、2,第二個就包含 3、4、5,第三個就包含 6、7、8。因為 IoTDB 每一個結果集都會有一列 Time,它顯示的時候也是用的起始時間,也就是我們的左端點去展示的。

但如果是左開右閉的話,那就是 0 不包含,它包含的時間點是 1 到 9。分組同樣的也會做改變:1、2、3;然后 4、5、6,然后 7、8、9。它做 Time 列的展示的時候,是會用右端點進行展示的,這里是 3、6、9。

2023大會田原圖15-20240109.png

降采樣跟大家提了一下,第二個就是剛剛我們說的分段的方式。這邊提到了一個定義叫差值分組,它是什么意思呢?就是它會根據所有的時間序列的值,跟這個組的第一個的差值去做分段。如果這個差值超過我們所設定的閾值了,那它就不屬于這個分組,它就屬于下一個分組了。從這張圖大家可以很容易的看出來,第一個值規定了這樣分組的一個區間,后面的值如果超過它比較多,那就明顯不屬于這個組了,它就會屬于下一個組。

2023大會田原圖16-20240109.png

這也是一個實際的例子。如果我們以 s1 這個序列去做分段,0.1 就是我們設定的閾值,從 0.9 跳到 1.2,差值已經是 0.3 了,所以它屬于下一個分組了。

2023大會田原圖17-20240109.png

這個 GROUP BY SESSION 也是在時序場景里面可能比較常見的,它叫會話分組。會話分組和剛剛不一樣的地方在于,它是對時間列去做分組操作的。比如說一臺機械,它可能并不是時時刻刻都是開機的,我想要它每一次開機的平均值。它的狀態可能一直是 1,就是它一直是開機的,你能看到數據,但是我要對它每一段開機做一個分段,那其實就是根據我們當前的這個時間點,它跟上一個時間點的間隔,如果超過了我們設定的閾值,我們就認為它中間可能發生了一些問題,或者發生過關機,那我們就可以把它認為是下一次開機時的狀態。這個其實也是比較常見的,我們是可以通過 GROUP BY SESSION 這樣一種方式去指定的。

2023大會田原圖18-20240109.png

這邊給了一個例子,比如說我們設定的閾值是 24 秒,這里第 4 行到第 5 行的時候,它的跨度超過了 24 秒,達到了 37 秒,那我們就把它自然而然地放到了下一個分組。

2023大會田原圖19-20240109.png

GROUP BY COUNT 這個函數很簡單,跟大家快速過一下,它就是按照固定的點數分組,比如說每 5 個點分一個組。

2023大會田原圖20-20240109.png

這里也提到,因為有一些 null 值,我們還有一些參數可以支持指定忽不忽略 null 值。

2023大會田原圖21-20240109.png

GROUP BY CONDITION 這個稍微有一些復雜,它其實可以讓用戶自定義一個分組條件,它的分組條件是通過 Expression 去指定的,如果計算出來的 Expression 是 true,也就是說滿足我們的自定義條件,那我們就把它保留下來,那些不滿足過濾條件的值可能就自動被過濾掉了,它不會放到我們的分組里面。

2023大會田原圖22-20240109.png

并且因為時序有天然的連續性,它還通過第二個參數去指定了我們連續滿足多少的時候,它才會被認定為我們需要選定的分組。

這里大家可以看到,我們指定 s1=1,并且它要連續超過兩個 s1=1 為 true 的時候,我們才保留它。可以看到這里只有兩個 group,即使第四行它得出來是 true,但是它連續的行數只有一個,所以它并不會被摘出來,所以我們得到的分組只有 group-1 跟 group-2。

2023大會田原圖23-20240109.png

其實剛才這個查詢也可以用 GROUP BY VARIATION 去實現,無非就是多一個 having,having 后面可能加一個 count(*)>=2,那它相比而言有什么好處呢?第一是它更加靈活,它支持很多這種自定義的條件,并且它的執行代價更小,因為我們不會對于不滿足分組的這些數據做聚合計算。我們如果用這種 GROUP BY VARIATION 的話,它其實是一開始得到很多很多的分組,那這些分組它也會做聚合計算,只不過最后在 having 的時候被過濾掉了。

2023大會田原圖24-20240109.png

剛剛講的都是我們分段、分組的一些方式,那這邊其實講了一個我們的標量函數,叫差值函數。這個也是在時序場景很常見,就是我們要求當前值跟上一個值的差值,這個在后面的一些例子里面我也會提到,這里給大家已經展示了一下,比如說 5-5=0。

2023大會田原圖25-20240109.png

還有就是剛剛提到,最后的空值填充的功能,我們除了能夠支持 PREVIOUS 填充之外,還能支持線性填充、常量填充等等,并且在前值填充的過程當中,我們還支持指定第二個參數,這個第二參數是什么意思呢?就是我們不是無腦地直接用前一個值去填充的,如果前一個真實的值跟當前的時間戳已經超過了我們指定的一個時間范圍,比如說 2 分鐘,我就認為它并不是一個有效值了,不需要去填充,我就繼續保持 null 就行了,所以在這一塊我們也可以支持指定第二個參數。

2023大會田原圖26-20240109.png

這是剛剛提到了,有三種填充方式。

2023大會田原圖27-20240109.png

除了剛剛說到的,我們還支持最新點查詢。最新點查詢的語法有一點特殊,只有這種語法去做的時候,它可以去命中我們的緩存,單個序列的最新點的查詢,它的延遲能夠達到微秒級別。

2023大會田原圖28-20240109.png

03 UDF 函數庫

第三個部分是我們的 UDF 函數庫的部分,主要跟大家介紹我們實用的一些 UDF。我剛剛提到宋老師組在做的 UDF 函數庫,我們現在一共有 59 個通用的函數,它包含數據質量函數、數據畫像函數、異常檢測、頻域分析,還有數據匹配、數據修復、序列發現,包括我們今天早上提到的機器學習的一些函數。

2023大會田原圖29-20240109.png

這里給大家舉幾個例子,比如說 ValueFill,這個其實跟剛剛的 FILL 很像,中間斷了一個我們可以給你自動的填充項。

2023大會田原圖30-20240109.png

還有就是我們的異常值修復,目前是支持兩種修復方式。這里可以看到,它可能有一些異常值會非常影響你的展示,上面這個圖中間這些數據趨勢都是平的了,但是其實把這些異常點給去掉之后,或者說修復之后,它展示出來的真正的趨勢應該是下面這個圖。這個還是在工業場景里面比較常用的 UDF 函數。

2023大會田原圖31-20240109.png

我們原來只支持行式的、迭代式的接口,在 1.3.0 版本里面,我們也支持了這種批量處理的接口,原來是一次處理一行,有 N 次的 function call,并且沒有向量化優化的空間,因為我們知道 JDK 越往后發展,可能也會有一些 SIMD 的一些接口,現在在最新 JDK 版本里已經有了,但還是預覽版。

2023大會田原圖32-20240109.png

那我們新的接口一次能夠處理一批數據,對于一批數據只有一次 function call,并且有向量優化的可能,沒有任何拆裝箱的開銷。

2023大會田原圖33-20240109.png

這里列了一下,我們原來用的是比較簡單的加法去實現,這是原來的接口。

2023大會田原圖34-20240109.png

這是現在的實現接口的方式。

2023大會田原圖35-20240109.png

結果是差距比較大的,如果我們用批量接口的話,性能能夠提升大約 6.5 倍。大家也可以試著把自己的一些舊的 UDF Function 更新成用現在的批量接口去實現,IoTDB 現在的函數庫里面,如果是能夠用這種優化去做的,我們都已經做了,大家也可以替換一下新的版本。

2023大會田原圖36-20240109.png

04 典型查詢場景示例

第四個部分,也是今天會重點跟大家介紹的,一些典型的查詢場景的實例。

第一個,剛剛其實也跟大家稍微提到一下,就是一些 UDF,它能夠做異常檢測,異常檢測是什么意思?就是它可能是一個序列里面,我們要找出異常的點,但對異常的這個點,每個人的定義方式可能是不一樣的。比如說這里,我定義異常的方式就是平均值的差距,超過我們 k 倍的標準差的數據的時候,我們會將它視為異常數據。右邊這個表里面也給大家展示了,我標紅的是會被選出來的異常數據。

2023大會田原圖37-20240109.png

那在 IoTDB 里面你可能不需要自己去實現這種 UDF 了,因為天然有這樣一個 ksigma 的函數,你直接去調用,指定這個 k 倍的 k 就夠了,它就能自動幫你去篩選出來了。

2023大會田原圖38-20240109.png

除了剛剛說的這個 ksigma 之外,還支持 IQR、LOF、MissDetect、Range、TwoSidedFilter、Outlier、MasterTrain,或者 MasterDetect 等等,總共是八種的異常檢測算法,當然這個異常檢測算法也在不斷地補充。

2023大會田原圖39-20240109.png

第二個其實剛剛也是提到過了,就是我們需要對數據進行降采樣,其實這個降采樣函數也是 GROUP BY 加上我們的 FILL 去做到的功能。比如這里,我想對數據進行整點,也就是整分鐘的這種采樣的時候,如果這個分鐘內,比如說 12:01 分,這個分鐘沒有數據,那怎么辦?我想要用離它最近的前一個點去做填充。

首先,既然是整點整分鐘,那肯定是跟 GROUP BY TIME 相關的了。如果沒有,再用前一個值填充,剛剛其實也提到過了,可能就用前值填充,FILL 的方式去做到。最后就是,我之前提到,如果前面一個值距離我當前這個值已經超過 3 分鐘了,我就不需要了,我就認為這個點就是沒有值的,你不需要再給我填充了。

2023大會田原圖40-20240109.png

那通過我剛剛提到的 GROUP BY TIME,確定一個時間范圍,然后第二個參數去指定降采樣的一個 1 分鐘的時間點,然后再通過 FILL PREVIOUS 這樣一個前值填充的方式,第二個參數去指定它填充的超時閾值,就能夠達到這樣一個效果。

最后得出來的結果,剛剛可以看到,上面是我們的原始數據結果,它只在 14:01:50、14:01:51、14:06:50 和 14:08:50 有數據,中間的 14:03 分、14:04 分、14:05 分,14:06 分這些,還包括 14:08 分、14:10 分的數據都是需要去填充的。只不過對于 14:05 分跟 14:06 分,這兩行數據是因為超過了我們的填充閾值,我們沒有對它進行填充。

2023大會田原圖41-20240109.png

第三點其實在工業里面也比較常見,就是我們每個機器去開啟跟關閉的時長。這個跟我剛剛說的還不一樣,我剛剛說的 GROUP BY SESSION,它可能對關閉的狀態就不會去采集數據了,但有一些機器,它關閉的時候也要去采集它的數據。所以它有一個字段就是專門用來標識它的關閉或開啟的狀態的,就是這個 on 字段,那它想做什么查詢呢?就是去查詢每一次機器它開啟和關閉的時長。

這里可以看到它雖然只有 0/1 值,但可以看到它每一次 0 的時候,連續的 0 的時間范圍是多少,連續的 1 的時間范圍是多少。那在關系型數據庫里面,使用 GROUP BY ON 的話,它沒有辦法區分第一段跟第三段,因為都是 0,這兩段會被放在一起。

2023大會田原圖42-20240109.png

在 IoTDB 里面,可能只需要用 GROUP BY VARIATION 就可以做到,因為 GROUP BY VARIATION 其實就是做這個的,它只要當前值跟上一個值不一樣,那不一樣是指多少呢?其實這里就是和 0 對比,默認就是 0。也就是說, 你只要不一樣,差值超過 0 了,我就認為你是下一個分組了。time_duration 就是一個我們的聚合函數,也是用來求這一段的最后一個值跟第一個值的時間戳的差值,也能夠達到我們這個求時長的效果。

2023大會田原圖43-20240109.png

第四點是一個車聯網場景里面比較常見的,它想要去查車的里程跳變的時刻。因為車的里程單位統計可能是用公里,開一段時間以后,里程就會一公里跳一下,那我們可能想要去篩選出來這些跳變的時刻,這就是里程跳變的含義。其實我們用肉眼去看的話,很容易找到這些里程跳變的時刻,比如圖中框出的這些 5、6、7、8,框出的 5 是第一個里程值,所以我們也會把它選出來。

2023大會田原圖44-20240109.png

這種情況下關系 SQL 該如何寫呢?因為最近 ChatGPT 比較火,我就去跟 ChatGPT 聊,我說你給我建一張關系表,這個關系表里面第一列是時間列,第二列是這個車列,即車的 ID,第三列是我剛剛說的 mileage,我跟它去描述了里程跳變的含義是什么,然后我讓它給我去寫一個 SQL。

該說不說,ChatGPT 還是很聰明的,它知道要用窗口函數去實現這樣一個功能,比如說這邊的 LAG 函數,就是去求當前行的上一行。因為關系數據庫中是無序的,所以這個窗口函數想要有序,它需要在窗口函數的子句里面去指定排序鍵,也就是這邊的 ORDER BY Time,它根據時間戳的排序是這樣排的。

2023大會田原圖45-20240109.png

但是 ChatGPT 其實忽略了一個事情,它需要人去慢慢給它提示,所以我告訴它,我只需要查一輛車的值,查詢的時候需要指定具體的 vinId,因為它前面其實是沒有指定 vinId 的。然后它就給出了一個比較正確的查詢,它在 where 子句里面指定了這個 vinId。

可以看到,用關系 SQL 寫出來,可能還要用嵌套、子查詢,包括 CTE 這些表達,這些寫出來已經是比較長的語句了,對一個不熟悉窗口函數的人來講,它可能還寫不出來這樣的方式。

2023大會田原圖46-20240109.png

而在 IoTDB 里面其實非常簡單,就是用我剛剛提到的 DIFF 函數,只要 DIFF 不等于 0,就證明里程發生跳變了,然后用 where time 指定一個時間區間,就能夠直接做到將里程跳變時刻的數據篩選出來了。

2023大會田原圖47-20240109.png

這里我還想舉一個例子,就是發生里程跳變之后,我們想要知道一天內發生里程跳變的次數。對于關系型數據庫,剛剛子查詢的部分,需要再做一個 COUNT。

2023大會田原圖48-20240109.png

那在 IoTDB 里面其實也比較容易做到,它不需要用任何子查詢也能做到,就是在我們的 COUNT 里面嵌套一個 CASE WHEN 的子句,只不過在 CASE WHEN 里面,我們指定了 DIFF 這樣一個標量函數,只要 DIFF 不等于 0,輸出結果就是 1,然后再把這些數起來就行了。

2023大會田原圖49-20240109.png

第五個典型的查詢還是車聯網場景的,需要去知道某臺車哪些天發生了定位異常,那定位異常的含義是什么?就是我們定義 GPS 的經緯度坐標都連續為 0 的次數大于 10 的時候,它才算一次定位異常。這里的數據其實都沒有定位異常。

2023大會田原圖50-20240109.png

那同樣的,我們問問 ChatGPT,上面場景的關系 SQL 該怎么去寫?我給它一個建模,它就給出來了這樣的查詢語句。當然它這個查詢語句肯定是錯的,因為它并沒有考慮到任何連續性的含義,我告訴它要連續超過十天才能是異常。它把這個語句寫在 HAVING 里面了,但它這個是不對的,所以它并沒有體現我們要求的連續 10 次才算異常的這樣一個語句。

2023大會田原圖51-20240109.png

我就再次告訴它需要這樣去做,然后它就給出了另外一個更復雜的操作。但它這個操作其實是有它自己的思考的,我們可以看一下它是想怎么去做的。它想要去給每條數據一個標號,首先它列出的 num_1 ,就是對應代碼這里的第三行,即 ROW_NUMBER 里面的第一個,被減數。它想去做的就是給每一行標一個順序,這樣一個從 1 到 8 的序號,然后它要根據每一個經緯度坐標做一個分組,每一個分組內去再做一個序號的標定,最后它想通過 num_1 減去 num_2 之后得到的這樣一個 group 進行標定。

2023大會田原圖52-20240109.png

我一開始思考了一下,它為什么要去這么做,大家可以看到,這里很明顯,我標黃的這個部分,其實它是一個連續為 0 的分組,2 減 1、3 減 2、4 減 3,得出來都是 1,所以 ChatGPT 想通過這樣一種方式,找到這樣連續的異常分組。

但是它忽略了一個事情。第一個是 where 的過濾是要優先于窗口函數執行的,所以在它做那個計算的時候,除了我標顏色的這些,其它的行都不會有,所以它從一開始就錯了。那即使按照它的這個方法,我把 where 條件過濾,放到后面去做,它也是錯的,因為大家可以看到最后一行,8 減 3 結果也是 5,但明顯它跟前面標綠的不是一個分組。

所以有時候 ChatGPT 也不是特別可信,如果用它來替代程序員或者 DBA 做一些查詢的話,大家目前也不用太擔心被取代以致失業的問題。

2023大會田原圖53-20240109.png

其實真正去寫關系 SQL 的話,可能寫出來會非常長。我們會用一些窗口函數去定義,因為要檢查 10 行,所以往前倒 10 個,寫出來是非常非常長的。

2023大會田原圖54-20240109.png

在 IoTDB 中其實是比較簡單的,這里就是用了 COUNT_IF 的功能,它主要就是在 COUNT_IF 里面去寫。因為 COUNT_IF 就是做了這樣一個事情,要數出來有多少個異常點,KEEP 連續大于等于 10 的時候,它才是定位異常點,并且要按天去做分組,去得到具體的日期,因為想知道的是哪一天的定位異常點。

2023大會田原圖55-20240109.png

第六個典型查詢,想要找出所有的車的休眠時間的首末條數據,并計算出時間差。那什么是休眠呢?就是當 gw_nm 這個字段不是 1 或者 0 的時候,我們都認為這個車處于休眠狀態。這邊數據標紅的就是休眠狀態,它有這種不規則的 “-”,或者 null 值,都被我們認為是休眠狀態,我們需要找出它的首末條數據,最后再計算出它的時間差。

2023大會田原圖56-20240109.png

我同樣也去問了一下 ChatGPT,但是大家可以忽略 ChatGPT 做出來的這個結果,因為它已經混亂了,在這種比較復雜語義的情況下,它寫出來的語句已經算是牛頭不對馬嘴了。

2023大會田原圖57-20240109.png

真正的關系型 SQL 寫法我也給大家列了一下,寫出來大概是這個樣子,雖然沒有剛剛的長,但它的思考過程可能比剛剛的要更復雜一點。大家可以看一下,在這里我就不跟大家展開介紹了,可以看到其實是比較復雜的,這邊只是得到了一張 temp 表,從這張表中,我們要過濾出來它開始的那一行和結束的那一行,這是 temp_a 表。然后 temp_b 表里面還要對它進行分組,得到它的分組 id,最后再根據我們剛剛得到的 id,去做這張表的自 JOIN,把它的首行和末行拼在同一行之后,我們的關系數據庫才能做這個時間差的計算。

所以這個關系 SQL 寫出來非常長,我這里是 3 頁 PPT,通過四張 temp 表才能夠去寫出來。

2023大會田原圖58-20240109.png

然而在 IoTDB 中依然比較簡單,這得益于我們的 GROUP BY CONDITION 函數。使用 GROUP BY CONDITION, gw_nm 這個字段不在 0、1 里面,并且它超過 2 次,我們就認為它是一個休眠分組了。然后只需要取到首條數據、末條數據,用我們的 FIRST_VALUE、LAST_VALUE。最后計算休眠時長,就是最后的時間戳減去第一個時間戳,就能得到我們的休眠時長。

2023大會田原圖59-20240109.png

最后一個典型查詢場景是要取出 60 秒以上的連續信號的第一條和最后一條數據,那這個連續信號怎么定義呢?就是我們的時間戳排序之后,一臺車的前后兩條數據的時間差不能夠超過 24 秒,其實剛剛講 GROUP BY SESSION 的時候可能跟大家提過了,這就是用 IoTDB 里面的 GROUP BY SESSION 去做的一個查詢。

2023大會田原圖60-20240109.png

在 IoTDB 里面很簡單,但是在關系型數據庫里面,可能它也會比較復雜。當然這里 ChatGPT 寫出來還是錯的,但是我也沒有去費時間給大家把這個關系 SQL 寫出來了。

2023大會田原圖61-20240109.png

給大家展示一下在 IoTDB 中寫出來的,會比較簡單,同樣的,FIRST_VALUE、LAST_VALUE 來取首末條數據,然后連續信號的時間跨度,使用 GROUP BY SESSION,即剛剛提到的連續信號的分組。并且我們需要持續時間超過 60 秒以上,可以通過 HAVING 的子句去做到。

OK,那我今天的演講就到這里了,謝謝。

2023大會田原圖62-20240109.png

更多內容推薦:

? 回顧 IoTDB 2023 大會全內容