在 Elasticsearch 中對時序型數據進行查詢和聚合
做者 Sonja Krause-Harderhtml
最初,Elasticsearch 是一個搜索引擎,將搜索索引保存在 Lucene 數據庫中。然而,Elasticsearch 從那時起已經歷了巨大演變,現已成爲一個高性能、集羣式的可擴展數據存儲。儘管從索引格式上仍能看出它的淵源,但 Elasticsearch 如今已被各種用戶普遍用於各類用途。docker
它的用途之一即是存儲、處理和檢索時序型數據。時序型數據的特色是每一個數據點都有一個相關的準確時間戳。最多見的情形是,一個數據點表明在具體時間點獲得的某種測量結果,能夠是股價、科學觀察值或者服務器負載。數據庫
儘管已有多個專門處理時序型數據的數據庫實施方案,可是在存儲和查詢時序型數據時仍沒有通用格式,並且理論上全部數據庫引擎都可用來處理時序型數據。編程
從抽象意義上講,一個時序型數據條目包括下列內容:ubuntu
在服務器監測用例中,確定會有一個鍵值對用以指明時序型數據屬於哪一個主機,但還能夠添加任何其餘信息,並且添加的這些信息以後可用來請求只獲取有關特定主機集合的指標。舉一些例子,運行特定服務的主機,僅屬於生產環境的主機,或者在特定雲服務提供商平臺上運行的實例。服務器
爲了更接地氣,咱們使用 Metricbeat 數據做爲示例來看一下如何使用 Elasticsearch 查詢來從數據中篩選出特定的時序型信息。網絡
每一個 Metricbeat 文檔都包括下列信息:less
真正的時序型數據elasticsearch
system
模塊的 cpu
指標集,請參見指標集文檔。文檔中所包含的有關指標自己的元數據。Metricbeat 使用 ECS 字段 event.module
和 event.dataset
來指定建立文檔時使用的是哪一個 Metricbeat 模塊,以及文檔中包含哪一個指標集。ionic
實例的相關元數據,其爲實體主機、虛擬機,仍是諸如 Kubernetes Pod 或 Docker 容器等小型實體
舉個例子,system.cpu
指標集中的 Metricbeat 文檔就是下面這個樣子。_source
對象的內聯註釋表示您能夠從哪裏獲取有關該字段的更多信息:
ECS 文檔
Metricbeat 文檔
system.cpu 指標集文檔
注意:爲便於理解,文檔在 JSON 文件中添加了 # 註釋。
{ "_index" : "metricbeat-8.0.0-2019.08.12-000001", "_type" : "_doc", "_id" : "vWm5hWwB6vlM0zxdF3Q5", "_score" :0.0, "_source" : { "@timestamp" :"2019-08-12T12:06:34.572Z", "ecs" : { # ECS metadata "version" :"1.0.1" }, "host" : { # ECS metadata "name" : "noether", "hostname" : "noether", "architecture" : "x86_64", "os" : { "kernel" :"4.15.0-55-generic", "codename" : "bionic", "platform" : "ubuntu", "version" :"18.04.3 LTS (Bionic Beaver)", "family" : "debian", "name" :"Ubuntu" }, "id" :"4e3eb308e7f24789b4ee0b6b873e5414", "containerized" : false }, "agent" : { # ECS metadata "ephemeral_id" :"7c725f8a-ac03-4f2d-a40c-3695a3591699", "hostname" : "noether", "id" : "e8839acc-7f5e-40be-a3ab-1cc891bcb3ce", "version" :"8.0.0", "type" : "metricbeat" }, "event" : { # ECS metadata "dataset" : "system.cpu", "module" : "system", "duration" :725494 }, "metricset" : { # metricbeat metadata "name" : "cpu" }, "service" : { # metricbeat metadata "type" : "system" }, "system" : { # metricbeat time series data "cpu" : { "softirq" : { "pct" :0.0112 }, "steal" : { "pct" :0 }, "cores" :8, "irq" : { "pct" :0 }, "idle" : { "pct" :6.9141 }, "nice" : { "pct" :0 }, "user" : { "pct" :0.7672 }, "system" : { "pct" :0.3024 }, "iowait" : { "pct" :0.0051 }, "total" : { "pct" :1.0808 } } } } }
總結一下,在 Metricbeat 文檔中,時序型數據和元數據混合在一塊兒,因此您需擁有文檔格式的具體相關知識才能準確檢索出所需的內容。
然而,若是您想處理、分析或可視化時序型數據,這些數據一般應該爲相似表格的形式,以下所示:
<series name> <timestamp> <value> <key-value pairs> system.cpu.user.pct 1565610800000 0.843 host.name=」noether」 system.cpu.user.pct 1565610800000 0.951 host.name=」hilbert」 system.cpu.user.pct 1565610810000 0.865 host.name=」noether」 system.cpu.user.pct 1565610810000 0.793 host.name=」hilbert」 system.cpu.user.pct 1565610820000 0.802 host.name=」noether」 system.cpu.user.pct 1565610820000 0.679 host.name=」hilbert」
Elasticsearch 查詢能幫助您經過編程方式以極其接近此類表格的形式檢索時序性數據,下面的例子爲您展現了操做過程。如想親自體驗查詢過程,您須要一個 Elasticsearch 實例,還須要安裝並運行 Metricbeat 以讓其爲 system.cpu
和 system.network
指標集傳輸數據。如需 Metricbeat 的簡短介紹,請參看ES網站中的入門文檔。
您能夠從 Kibana 中的開發工具 (Dev Tools) 控制檯運行全部查詢。如以前並未用過,您能夠查看ES網站中 Kibana 控制檯文檔簡單瞭解一下。請注意,您須要更改示例查詢中的主機名。
咱們假設您已按照默認配置將 Metricbeat 設置完畢。這即表示它天天會建立一個索引,並且這些索引的命名規則爲「metricbeat-版本號-日期-計數」,例如 metricbeat-7.3.0-2019.08.06-000009
。要一次性查詢全部這些索引,咱們須要使用通配符:
示例查詢:
GET metricbeat-*/_search
示例響應以下:
{ "took" :2, "timed_out" : false, "_shards" : { "total" :1, "successful" :1, "skipped" :0, "failed" :0 }, "hits" : { "total" : { "value" :10000, "relation" : "gte" }, "max_score" :1.0, "hits" : [...] } }
很明顯,該查詢超出了 Elasticsearch 在單次查詢中可返回文檔的數量限制。這裏省略了真實命中信息,但您可能但願滾動查詢結果並與上面已加註釋的文檔進行比較。
根據所監測基礎設施的規模,雖然可能會有海量的 Metricbeat 文檔,但您不多須要從最開始(記錄)的時間點查詢時序型數據,因此咱們開始時用一個日期範圍,在本案例中是過去 5 分鐘:
示例查詢:
GET metricbeat-*/_search { "query": { "range": { "@timestamp": { "gte": "now-5m" } } } }
示例響應以下:
{ "took" :4, "timed_out" : false, "_shards" : { "total" :1, "successful" :1, "skipped" :0, "failed" :0 }, "hits" : { "total" : { "value" :30, "relation" : "eq" }, "max_score" :0.0, "hits" : [...] } }
這個規模管理起來要容易得多。然而,運行此查詢時所基於的系統僅有一個主機向其報告,因此在生產環境中,命中數仍然會很高。
如要檢索特定主機的全部 CPU 數據,進行 Elasticsearch 查詢時第一步原生嘗試多是針對 host.name 和指標集 system.cpu
添加篩選:
示例查詢:
GET metricbeat-*/_search { "query": { "bool": { "filter": [ { "range": { "@timestamp": { "gte": "now-5m" } } }, { "bool": { "should": [ { "match_phrase": { "host.name": "noether" } }, { "match_phrase": { "event.dataset": "system.cpu" } } ] } } ] } } }
示例響應以下:
{ "took" :8, "timed_out" : false, "_shards" : { "total" :1, "successful" :1, "skipped" :0, "failed" :0 }, "hits" : { "total" : { "value" :30, "relation" : "eq" }, "max_score" :0.0, "hits" : [...] } }
此查詢仍會返回大量文檔,全部都包含 Metricbeat 發送的有關 system.cpu
指標集的完整數據。這個結果的用處並不大,緣由以下。
首先,咱們須要檢索整個時間範圍內的全部文檔。一旦咱們達到所配置的上限,Elasticsearch 將不會一次性返回這些結果;它會嘗試對文檔排序,這對咱們的查詢根本不適用;同時 Elasticsearch 返回結果時不會按時間戳進行排序。
第二,咱們僅對每一個文檔中的一小部份內容感興趣:時間戳、幾個指標值,可能還有一些其餘元數據字段。從 Elasticsearch 中返回所有的 _source
,而後再從查詢結果中挑選數據,這種方法的效率很低。
解決這種問題的方法之一就是利用 Elasticsearch _聚合_。
**
**
咱們首先看一下日期直方圖。日期直方圖聚合將會爲每一個時間間隔返回一個值。返回的桶已按時間進行排序,並且用戶能夠指定間隔(又稱桶大小)來匹配數據。在這個示例中,咱們將間隔時長選爲 10 秒,由於 Metricbeat 默認每 10 秒從系統模塊發送一次數據。頂層的 size: 0
參數表示咱們對實際命中結果再也不感興趣,而只對聚合感興趣,因此不會返回任何文檔。
示例查詢:
GET metricbeat-*/_search { "query": {...}, # same as above "size":0, "aggregations": { "myDateHistogram": { "date_histogram": { "field": "@timestamp", "fixed_interval":"10s" } } } }
示例響應以下:
{ ..., "hits" : { "total" : { "value" :30, "relation" : "eq" }, "max_score" : null, "hits" : [ ] }, "aggregations" : { "myDateHistogram" : { "buckets" : [ { "key_as_string" :"2019-08-12T13:03:20.000Z", "key" :1565615000000, "doc_count" :1 }, { "key_as_string" :"2019-08-12T13:03:30.000Z", "key" :1565615010000, "doc_count" :1 }, { "key_as_string" :"2019-08-12T13:03:40.000Z", "key" :1565615020000, "doc_count" :1 }, ... ] } } }
對於每一個桶,這會在 key
中返回時間戳,還會返回頗有幫助的 key_as_string
(其中包含用戶可讀的日期時間字符串),以及桶中包含的文檔數量。
這個案例的 doc_count
是 1,由於桶大小與 Metricbeat 的報告期間相匹配。若是沒有其餘信息,這一結果並沒什麼用處,因此爲了看到真正的指標值,咱們須要再添加一個聚合。在這一步,咱們須要決定聚合類型——對數值而言,avg
、min
和 max
都是不錯的選擇——但因爲咱們每一個桶只有一個文檔,因此不管選擇哪一個都沒什麼影響。下面的示例便很好地展現了這一點,由於它針對指標 system.cpu.user.pct
在桶的 10 秒期間內的值返回了 avg
、min
和 max
聚合:
示例查詢:
GET metricbeat-*/_search { "query": {...}, # same as above "size":0, "aggregations": { "myDateHistogram": { "date_histogram": { "field": "@timestamp", "fixed_interval":"10s" }, "aggregations": { "myActualCpuUserMax": { "max": { "field": "system.cpu.user.pct" } }, "myActualCpuUserAvg": { "avg": { "field": "system.cpu.user.pct" } }, "myActualCpuUserMin": { "min": { "field": "system.cpu.user.pct" } } } } } }
示例響應以下:
{ ..., "hits" : {...}, "aggregations" : { "myDateHistogram" : { "buckets" : [ { "key_as_string" :"2019-08-12T13:12:40.000Z", "key" :1565615560000, "doc_count" :1, "myActualCpuUserMin" : { "value" :1.002 }, "myActualCpuUserAvg" : { "value" :1.002 }, "myActualCpuUserMax" : { "value" :1.002 } }, { "key_as_string" :"2019-08-12T13:12:50.000Z", "key" :1565615570000, "doc_count" :1, "myActualCpuUserMin" : { "value" :0.866 }, "myActualCpuUserAvg" : { "value" :0.866 }, "myActualCpuUserMax" : { "value" :0.866 } }, ... ] } } }
您能夠看到,在每一個桶中 myActualCpuUserMin
、myActualCpuUserAvg
和 myActualCpuUserMax
是同樣的,因此若是須要檢索按固定間隔報告的時序型數據的原始值,您可使用日期直方圖來實現。
然而,您多數狀況下不會對每一個單獨的數據點感興趣,若是每隔幾秒便取一個測量值的話,則更是如此。出於不少目的,實際上更好的一種方法是擁有更粗粒度的數據:舉個例子,若是一個可視化僅有限定數量的像素來展現時序型數據的變化,那麼其在呈現時便會捨棄較細粒度的數據。
咱們一般會縮小時序型數據的採樣,直至其粒度與任何後續處理步驟的要求相符。在縮小採樣的過程當中,給定時間段內的多個數據點會縮減爲一個點。在咱們的服務器監測示例中,數據的測量頻率爲每 10 秒一次,但多數狀況下,一分鐘內全部值的平均值應該就能夠。偶爾狀況下,縮小採樣的過程與日期直方圖聚合的過程如出一轍,前提是日期直方圖聚合過程爲每一個桶找到多於一份文檔,而且應用了正確的嵌套聚合。
下面的示例展現了在完整的 1 分鐘桶內採用嵌套 avg
、min
和 max
聚合的日期直方圖結果,給出了縮小採樣的第一個示例。因爲使用了 calendar_interval
,而未使用 fixed_interval
,因此此參數會將桶邊界調整爲整分鐘。
示例查詢:
GET metricbeat-*/_search { "query": {...}, # same as above "size":0, "aggregations": { "myDateHistogram": { "date_histogram": { "field": "@timestamp", "calendar_interval":"1m" }, "aggregations": { "myDownsampledCpuUserMax": { "max": { "field": "system.cpu.user.pct" } }, "myDownsampledCpuUserAvg": { "avg": { "field": "system.cpu.user.pct" } }, "myDownsampledCpuUserMin": { "min": { "field": "system.cpu.user.pct" } } } } } }
示例響應以下:
{ ..., "hits" : {...}, "aggregations" : { "myDateHistogram" : { "buckets" : [ { "key_as_string" :"2019-08-12T13:27:00.000Z", "key" :1565616420000, "doc_count" :4, "myDownsampledCpuUserMax" : { "value" :0.927 }, "myDownsampledCpuUserMin" : { "value" :0.6980000000000001 }, "myDownsampledCpuUserAvg" : { "value" :0.8512500000000001 } }, { "key_as_string" :"2019-08-12T13:28:00.000Z", "key" :1565616480000, "doc_count" :6, "myDownsampledCpuUserMax" : { "value" :0.838 }, "myDownsampledCpuUserMin" : { "value" :0.5670000000000001 }, "myDownsampledCpuUserAvg" : { "value" :0.7040000000000001 } }, ... ] } } }
如您所見,myActualCpuUserMin
、myActualCpuUserAvg
和 myActualCpuUserMax
如今的值不一樣,具體取決於所用的聚合。
縮小採樣時採用哪一種方法與指標息息相關。對於 CPU 百分比,一分鐘內的 avg
聚合就能夠,針對諸如隊列時長和系統負載等指標,max
聚合則可能更爲合適。
如今,還可使用 Elasticsearch 來執行一些簡單的代數運算,並計算原始數據中沒有的時序型數據。假設咱們針對 CPU 進行 avg
聚合,則咱們的示例可加以優化以返回用戶 CPU、系統 CPU,以及用戶和系統總和除以 CPU 核數,命令以下:
示例查詢:
GET metricbeat-*/_search { "query": {...}, # same as above "size":0, "aggregations": { "myDateHistogram": { "date_histogram": { "field": "@timestamp", "calendar_interval":"1m" }, "aggregations": { "myDownsampledCpuUserAvg": { "avg": { "field": "system.cpu.user.pct" } }, "myDownsampledCpuSystemAvg": { "avg": { "field": "system.cpu.system.pct" } }, "myCpuCoresMax": { "max": { "field": "system.cpu.cores" } }, "myCalculatedCpu": { "bucket_script": { "buckets_path": { "user": "myDownsampledCpuUserAvg", "system": "myDownsampledCpuSystemAvg", "cores": "myCpuCoresMax" }, "script": { "source": "(params.user + params.system) / params.cores", "lang": "painless" } } } } } } }
示例響應以下:
{ ..., "hits" : {...}, "aggregations" : { "myDateHistogram" : { "buckets" : [ { "key_as_string" :"2019-08-12T13:32:00.000Z", "key" :1565616720000, "doc_count" :2, "myDownsampledCpuSystemAvg" : { "value" :0.344 }, "myCpuCoresMax" : { "value" :8.0 }, "myDownsampledCpuUserAvg" : { "value" :0.8860000000000001 }, "myCalculatedCpu" : { "value" :0.15375 } }, { "key_as_string" :"2019-08-12T13:33:00.000Z", "key" :1565616780000, "doc_count" :6, "myDownsampledCpuSystemAvg" : { "value" :0.33416666666666667 }, "myCpuCoresMax" : { "value" :8.0 }, "myDownsampledCpuUserAvg" : { "value" :0.8895 }, "myCalculatedCpu" : { "value" :0.15295833333333334 } }, ... ] } } }
**
**
關於 Elasticsearch 聚合在處理時序型數據時的巨大做用,咱們還能夠舉一個更加詳細的關於 system.network
指標集的示例。system.network
指標集文檔中的相關部分以下所示:
{ ... "system": { "network": { "in": { "bytes":37904869172, "dropped":32, "errors":0, "packets":32143403 }, "name": "wlp4s0", "out": { "bytes":6299331926, "dropped":0, "errors":0, "packets":13362703 } } } ... }
Metricbeat 會爲系統中存在的每一個網絡接口發送一份文檔。這些文檔的時間戳相同,可是 system.network.name
字段的值不一樣,每一個網絡接口都有一個值。
任何進一步的聚合都須要按照接口完成,因此咱們針對 system.network.name
字段將上個例子中頂層的日期直方圖聚合更改成多值聚合。
請注意如要此方法奏效,須要將所聚合的字段映射爲關鍵字字段。若是您使用 Metricbeat 提供的默認索引模板,則此映射可能已爲您設置完畢。如未設置,Metricbeat 模板文檔頁面就您須要完成的操做給出了簡短描述。
示例查詢:
GET metricbeat-*/_search { "query": {...}, # same as above "size":0, "aggregations": { "myNetworkInterfaces": { "terms": { "field": "system.network.name", "size":50 }, "aggs": { "myDateHistogram": { "date_histogram": { "field": "@timestamp", "calendar_interval":"1m" } } } } } }
示例響應以下:
{ ..., "hits" : {...}, "aggregations" : { "myNetworkInterfaces" : { "doc_count_error_upper_bound" :0, "sum_other_doc_count" :0, "buckets" : [ { "key" : "docker0", "doc_count" :29, "myDateHistogram" : { "buckets" : [...] } }, { "key" : "enp0s31f6", "doc_count" :29, "myDateHistogram" : { "buckets" : [...] } }, { "key" : "lo", "doc_count" :29, "myDateHistogram" : { "buckets" : [...] } }, { "key" : "wlp61s0", "doc_count" :29, "myDateHistogram" : { "buckets" : [ { "key_as_string" :"2019-08-12T13:39:00.000Z", "key" :1565617140000, "doc_count" :1 }, { "key_as_string" :"2019-08-12T13:40:00.000Z", "key" :1565617200000, "doc_count" :6 }, { "key_as_string" :"2019-08-12T13:41:00.000Z", "key" :1565617260000, "doc_count" :6 }, { "key_as_string" :"2019-08-12T13:42:00.000Z", "key" :1565617320000, "doc_count" :6 }, { "key_as_string" :"2019-08-12T13:43:00.000Z", "key" :1565617380000, "doc_count" :6 }, { "key_as_string" :"2019-08-12T13:44:00.000Z", "key" :1565617440000, "doc_count" :4 } ] } }, ... ] } } }
和 CPU 示例同樣,不使用嵌套聚合的話,日期直方圖聚合僅會返回用處不太大的 doc_count
。
字節字段包含單調遞增值。這些字段的值包括自機器上次啓動以來所發送或接收的字節數,因此每次測量時該值都會增大。在這個案例中,正確的嵌套聚合是 max
,因此縮小採樣後的值要包括最高值,也就是桶間隔期間所獲得的最新測量值。
示例查詢:
GET metricbeat-*/_search { "query": {...}, # same as above "size":0, "aggregations": { "myNetworkInterfaces": { "terms": { "field": "system.network.name", "size":50 }, "aggs": { "myDateHistogram": { "date_histogram": { "field": "@timestamp", "calendar_interval":"1m" }, "aggregations": { "myNetworkInBytesMax": { "max": { "field": "system.network.in.bytes" } }, "myNetworkOutBytesMax": { "max": { "field": "system.network.out.bytes" } } } } } } } }
示例響應以下:
{ ..., "hits" : {...}, "aggregations" : { "myNetworkInterfaces" : { "doc_count_error_upper_bound" :0, "sum_other_doc_count" :0, "buckets" : [ { "key" : "docker0", ... }, { "key" : "enp0s31f6", ... }, { "key" : "lo", ... }, { "key" : "wlp61s0", "doc_count" :30, "myDateHistogram" : { "buckets" : [ { "key_as_string" :"2019-08-12T13:50:00.000Z", "key" :1565617800000, "doc_count" :2, "myNetworkInBytesMax" : { "value" :2.991659837E9 }, "myNetworkOutBytesMax" : { "value" :5.46578365E8 } }, { "key_as_string" :"2019-08-12T13:51:00.000Z", "key" :1565617860000, "doc_count" :6, "myNetworkInBytesMax" : { "value" :2.992027006E9 }, "myNetworkOutBytesMax" : { "value" :5.46791988E8 }, "myNetworkInBytesPerSecond" : { "value" :367169.0, "normalized_value" :6119.483333333334 }, "myNetworkoutBytesPerSecond" : { "value" :213623.0, "normalized_value" :3560.383333333333 } }, ... ] } }, ... ] } } }
如要從單調遞增計數中得到每秒的字節速率,須要使用導數聚合。當此聚合收到所傳遞的可選參數 unit
時,會在 normalized_value
字段中返回理想的每單位的數值:
示例查詢:
GET metricbeat-*/_search { "query": {...}, # same as above "size":0, "aggregations": { "myNetworkInterfaces": { "terms": { "field": "system.network.name", "size":50 }, "aggs": { "myDateHistogram": { "date_histogram": { "field": "@timestamp", "calendar_interval":"1m" }, "aggregations": { "myNetworkInBytesMax": { "max": { "field": "system.network.in.bytes" } }, "myNetworkInBytesPerSecond": { "derivative": { "buckets_path": "myNetworkInBytesMax", "unit":"1s" } }, "myNetworkOutBytesMax": { "max": { "field": "system.network.out.bytes" } }, "myNetworkoutBytesPerSecond": { "derivative": { "buckets_path": "myNetworkOutBytesMax", "unit":"1s" } } } } } } } }
示例響應以下:
{ ..., "hits" : {...}, "aggregations" : { "myNetworkInterfaces" : { "doc_count_error_upper_bound" :0, "sum_other_doc_count" :0, "buckets" : [ { "key" : "docker0", ... }, { "key" : "enp0s31f6", ... }, { "key" : "lo", ... }, { "key" : "wlp61s0", "doc_count" :30, "myDateHistogram" : { "buckets" : [ { "key_as_string" :"2019-08-12T14:07:00.000Z", "key" :1565618820000, "doc_count" :4, "myNetworkInBytesMax" : { "value" :3.030494669E9 }, "myNetworkOutBytesMax" : { "value" :5.56084749E8 } }, { "key_as_string" :"2019-08-12T14:08:00.000Z", "key" :1565618880000, "doc_count" :6, "myNetworkInBytesMax" : { "value" :3.033793744E9 }, "myNetworkOutBytesMax" : { "value" :5.56323416E8 }, "myNetworkInBytesPerSecond" : { "value" :3299075.0, "normalized_value" :54984.583333333336 }, "myNetworkoutBytesPerSecond" : { "value" :238667.0, "normalized_value" :3977.7833333333333 } }, { "key_as_string" :"2019-08-12T14:09:00.000Z", "key" :1565618940000, "doc_count" :6, "myNetworkInBytesMax" : { "value" :3.037045046E9 }, "myNetworkOutBytesMax" : { "value" :5.56566282E8 }, "myNetworkInBytesPerSecond" : { "value" :3251302.0, "normalized_value" :54188.36666666667 }, "myNetworkoutBytesPerSecond" : { "value" :242866.0, "normalized_value" :4047.766666666667 } }, ... ] } }, ... ] } } }
您可在本身的集羣上嘗試全部命令,若是您尚未集羣,能夠免費試用基於 Elastic Cloud 的 Elasticsearch Service 並快速部署一個集羣,您還能夠下載 Elastic Stack 的默認分發包。使用 Metricbeat 開始從您的系統發送數據吧,暢享查詢的樂趣!