MongoDB - 索引知識

来源:https://www.cnblogs.com/fatedeity/archive/2022/11/18/16902059.html
-Advertisement-
Play Games

學會 MongoDB 的增刪改查只能算得上是“初窺門徑”,瞭解、熟練掌握索引才能算得上“融會貫通”。基本可以認為資料庫的索引知識是一個初級開發向中級開發轉變所必備的知識。 ...


索引簡介

什麼是索引

索引最常用的比喻就是書籍的目錄,查詢索引就像查詢一本書的目錄。

索引支持 MongoDB 查詢的高效執行。如果沒有索引,MongoDB 必須掃描集合中每一個文檔,以選擇與查詢語句相匹配的文檔。如果查詢存在適當的索引,MongoDB 就可以使用索引來限制它掃描的文檔數。

篩選欄位時使用索引提速有以下幾個因素:

  • 索引數據通過 B 樹來存儲,從而使得搜索的時間複雜度為 \(O(\log n)\)
  • 索引本身存儲在高速緩存中,相比磁碟 IO 有大幅的性能提升(有的時候數據量非常大的時候,索引數據也會非常大,當大到超出記憶體容量的時候,會導致部分索引數據存儲在磁碟上,這會導致磁碟 IO 的開銷大幅增加,從而影響性能,所以務必要保證有足夠的記憶體能容下所有的索引數據)

索引可以顯著縮短查詢時間,但是使用索引、維護索引是有代價的。在執行寫入操作時,除了要更新文檔之外,還必須更新索引,這必然會影響寫入的性能。

因此,當有大量寫操作而讀操作少時,或者不考慮讀操作的性能時,都不推薦建立索引。

何時不使用索引

查詢結果集在原集合中占比越大,索引就會越低效。

出現這種情況的原因是:使用索引需要進行兩次查找:一次是查找索引項,一次是根據索引的指針去查找其指向的文檔。而全表掃描只需進行一次查找:查找文檔。在最壞的情況下(返回集合內的所有文檔),使用索引進行查找的次數會是全表掃描的兩倍,通常會明顯比全表掃描慢。

根據經驗,如果查詢返回集合中 30% 或更少的文檔,則索引通常可以加快速度。然而,這個數字會在 2%~60% 變動。

通常,索引使用的情況有這些:比較大的集合、比較大的文檔、選擇性查詢。

而全表掃描相對使用的情況有這些:比較小的集合、比較小的文檔、非選擇性查詢。

總結一下就是,在以下情況不推薦使用索引:

  • 有大量寫操作而讀操作較少的場景,更新損耗比查詢的損耗更大,不推薦建索引
  • 查詢結果集在原集合中占比越大,索引就會越低效
  • 索引基數(欄位去重後的數量)越低,索引的作用就越小

MongoDB 如何選擇索引

MongoDB 如何選擇索引具有自己的機制,通常是根據要搜索的欄位和一些附加信息(比如是否有排序)有關。基於這些信息,系統會識別出可能用於滿足查詢的候選索引。

當候選索引被選出之後,則會進行候選索引競賽的階段。

在競賽階段,MongoDB 會分別為這些候選索引創建 1 個查詢計劃,併在並行線程中運行這些查詢計劃,每個線程使用不同的索引。

候選索引競賽階段

到達目標狀態的第一個查詢計劃成為贏家。更重要的是,具有相同 形狀 的其他查詢都會選擇這個索引。

服務端會維護這些查詢計劃的緩存,以備將來用於進行相同 形狀 的查詢。

通常,以下這些事件導致緩存被清除掉:隨著時間變化、重建特定的索引、添加或刪除索引、顯式清除計劃緩存、mongod 進程的重啟等。

索引的類型

單一索引

單一索引

MongoDB 提供了預設的 _id 索引,在此之外,還支持對文檔的單個欄位創建用戶定義的升序、降序索引。但是對於單欄位索引,索引鍵的排序順序並不重要,因為 MongoDB 可以在任意方向上遍歷索引。

複合索引

複合索引

MongoDB 還支持在多個欄位上定義索引,即複合索引。

在考慮複合索引的設計時,需要知道對於利用索引的通用查詢模式,如何處理其等值過濾、多值過濾以及排序這些部分。大部分情況可以參考以下準則:

  • 等值過濾欄位應該在最前面
  • 排序欄位應在多值過濾欄位之前
  • 多值過濾欄位應該在最後面

多鍵索引

多鍵索引

多鍵索引和複合索引的概念不能搞混,如果一個文檔有被索引的數組欄位,則該索引會立即被標記為多鍵索引。

對數組創建索引就是對數組中的每個元素創建索引,而不是對數組本身創建索引。

對數組創建索引有一個例外,即 MongoDB 最多支持對一個數組欄位創建索引,索引項中不允許出現多個數組欄位,這是為了避免多鍵索引中的索引項數量呈爆炸式地增長。

多鍵索引通常會比非多鍵索引慢一些,可能會有許多索引項指向同一個文檔。而一旦索引被標記為多鍵多鍵,就再也無法變成非多鍵索引,唯一辦法是將多鍵索引刪除重建成非多鍵索引。

地理空間索引

為了支持對地理空間坐標數據的高效查詢,MongoDB 提供了兩個特殊索引:返回結果時使用平面幾何的 2d 索引和使用球面幾何的 2dsphere 索引。

創建索引時,通過將索引鍵的值設置成 2d 或者是 2dsphere 即可創建地理空間索引。

文本索引

MongoDB 提供了一種文本索引類型,支持在集合中搜索字元串內容。這些文本索引不存在特定於語言的停用詞(如 theaor 等),並且集合中的詞乾僅存儲詞根。

文本索引需要一定數量的與被索引欄位中單詞成比例的鍵,創建文本索引可能會耗費大量的系統資源。同時寫操作通常比對單一索引、符合索引,甚至多鍵索引的寫操作開銷更大,應在需求明確時創建文本索引。

創建索引時,通過將索引鍵的值設置成 text 即可創建文本索引,並且可以同時對多個鍵創建文本索引。在創建文本索引時,也可以用 $** 表示文檔的所有字元串欄位。

預設情況下,文本索引中的每個欄位都會被平等對待。也可以通過 wights 屬性設置本文索引中每個鍵的權重。但需要註意的是,文本索引一旦被創建,就不能改變索引的權重了(除非刪除索引再重建)。

文本索引能解決搜索關鍵字的問題,但對於在中國使用漢字的應用程式來說,請謹慎使用。從 官方文檔 中可以瞭解到支持到語言,其中並沒有包含漢字。

哈希索引

為了支持基於哈希的分片,MongoDB 提供了哈希索引類型,索引欄位值的哈希值。這些索引在其範圍內具有更隨機的值分佈,但僅支持等值匹配而不支持範圍查詢。

對於嵌入文檔,哈希索引的哈希函數會摺疊其值並計算哈希值,而對於數組,哈希索引是不支持的,對其創建哈希索引時會返回錯誤。

MongoDB 不支持在哈希索引上指定唯一約束,可以通過對存儲原始值的鍵構建唯一索引以指定唯一約束。

在創建索引時,通過將鍵的值設置為 hashed 即可將其設置成哈希索引。

索引的屬性

唯一索引

索引的唯一屬性會導致 MongoDB 拒絕索引欄位的重覆值。除了唯一約束之外,唯一索引在功能上可與其他 MongoDB 索引相同。

對於單一索引,唯一屬性針對的是單個鍵值;對於複合索引,唯一屬性針對的是所有鍵值的組合。

在某些情況下,索引桶(index bucket)的大小是有限制的,如果索引項超過了索引桶的大小就不會被包含在索引中。

在 MongoDB 4.2 之前,索引中包含的欄位必須小於 1024 位元組,也就是說大小超過 1024 位元組的鍵不會受到唯一索引的約束;在 MongoDB 4.2 及以後版本,這個限制被去掉了。

創建索引時設置 {unique: true} 可以設置唯一索引。

部分索引

部分索引在 3.2 版本新增,其表示僅索引符合特定過濾表達式的文檔。

MongoDB 的部分索引只會在數據的一個子集上創建,通過索引集合中的文檔子集,部分索引具有較低的存儲要求,可以減少索引創建和維護的性能成本。

創建索引時設置 partialFilterExpression 選項,可以只對符合表達式要求的值做索引。

當查詢條件匹配部分索引時,不在索引內的值不在搜索結果當中,如果需要返回那些缺少欄位的文檔,可以使用 hint 強制執行全表掃描。

稀疏索引

稀疏索引也稱為間隙索引,就是包含具有索引欄位的文檔的條目,跳過沒有索引欄位的文檔。

通過上述的定義可以看出,稀疏索引是部分索引的子集,創建部分索引時設定索引鍵必須存在的過濾表達式即可達到稀疏索引的作用。

將稀疏索引和唯一索引組合,以拒絕具有欄位重覆值的文檔,但忽略沒有索引鍵的文檔。

創建索引時設置 {sparse: true} 可以設置稀疏索引。

TTL 索引

TTL 索引提供了一個過期機制,允許為每一個文檔設置一個過期時間,當一個文檔達到預設的過期時間之後就會被刪除。

TTL 索引有自己適合的場景,如機器生成的事件數據,日誌和會話信息等,這些信息通常只需在資料庫中保存有限的時間。

MongoDB 會每分鐘掃描一次 TTL 索引,因此不應依賴於秒級的粒度。

創建索引時設置 {expireAfterSeconds: <seconds>} 可以設置 TTL 索引,通常索引鍵時日期類型時,TTL 索引才會起作用。

MongoDB 還提供了一種類似於固定長度隊列的集合,稱作為“固定集合”。其長度是固定的,當集合已滿足設定大小時,舊的文檔會被刪除,新的文檔將取而代之。

通常來說,相對於固定集合,MongoDB 優先推薦使用 TTL 索引,因為其在 WiredTiger 存儲引擎(在 3.2 版本開始作為預設存儲引擎)中性能更好,可操作性也更強。

不區分大小寫的索引

在 3.4 版本,MongoDB 提供了不區分大小寫索引屬性,支持在不考慮大小寫的情況下執行字元串比較的查詢。

創建索引時通過設置 {collation: {locale : <locale>, strength : <strength>}} 可以創建不區分大小寫的索引。

其中,locale 指定語言規則,可以通過 官方文檔 查看更多,使用 strength 可以指定比較級別,可以通過 官方文檔 瞭解更詳細內容。

索引的使用

管理索引

索引的所有信息都存儲在 system.indexes 集合中,這是一個保留集合,不支持修改或刪除,只能通過相關命令對其進行操作。

MongoDB 提供了一些相關命令管理索引,以下是常用的方法:

  • db.collection.createIndex(keys, options, commitQuorum): 創建單個索引
  • db.collection.createIndexes([keyPatterns], options, commitQuorum): 創建多個索引
  • db.collection.dropIndex(index): 刪除集合中除 _id 的指定索引
  • db.collection.dropIndexes(): 不傳參時可以刪除集合中除 _id 的全部索引,也可以指定索引名實現刪除指定索引
  • db.collection.getIndexes(): 查詢集合的索引信息
  • db.collection.hideIndex(<index>): 在 4.4 版本新增,隱藏索引對查詢計劃器不可見,不能用於查詢,可以通過隱藏索引發現在不刪除索引的情況下評估刪除所有的潛在影響
  • db.collection.unhideIndex(<index>): 取消隱藏索引

MongoDB 的索引名稱可標識索引,大部分的索引管理命令都支持使用名稱指定索引。

索引名稱的預設形式是 keyname1dir1_keyname2_dir2..._keynameN_dirN,其中 keynameX 是索引的鍵,dirX 是索引的方向(1-1)。

索引名稱是有字元數限制的,並且比較多的鍵時也會難以辨識,因此創建複雜的索引時可以自定義名稱。

修改索引

當需要修改索引時,通常的做法是先使用 dropIndex(index) 刪除指定索引,再使用 createIndex 重建索引。

修改索引的操作一般發生在應用程式已經上線之後,這時就需要考慮到創建索引既耗時又耗資源,考慮使用 background 選項在後臺創建索引,儘可能減少對讀寫操作的影響。

在 MongoDB 4.2 之後,引入了混合索引創建的機制,即在索引創建的開始和結束時持有排他鎖,創建過程中其餘部分會交錯地讓步於讀寫操作。

索引方向

使用單一索引時,索引鍵的方向並不重要,MongoDB 會根據排序的方向,選擇掃描索引的方向。只有基於多個查詢條件進行排序時,索引方向才是重要的。

對於複合索引,有可能對不同的鍵設置不同的方向,這與實際的業務有關係。

通常是創建與排序方向相同的索引方向,且相互反轉(在每個方向上都乘以 -1)的索引是等價的:{age: 1, username: -1} 適用的查詢與 {age: -1, username: 1} 完全一樣。

索引基數

索引的基數是指集合中某個欄位有多少個不同的值,即值去重後的數量。

通常來說,一個欄位的基數越高,這個欄位上的索引就越有用。對於基數比較低的欄位,索引通常無法排除大量可能的匹配項。

一個例子就是,如果對“性別”欄位創建索引,而查找“男性”時僅能將搜索空間縮小大約 50%,其索引作用相對是較低的。

根據經驗來說,應該在基數比較高的鍵上創建索引,或者至少應該把基數比較高的鍵放在複合索引的前面(在低基數的鍵之前)。

左首碼原則

MongoDB 的複合索引遵循左首碼原則:擁有多個鍵的索引,可以同時得到所有這些鍵的首碼組成的索引,但不包括除左首碼之外的其他子集。

比如說,有一個類似 {a: 1, b: 1, c: 1, ..., z: 1} 這樣的索引,那麼實際上也等於有了 {a: 1}{a: 1, b: 1}{a: 1, b: 1, c: 1} 等一系列索引,但是不會有 {b: 1} 這樣的非左首碼的索引。

交叉索引

在 2.6 版本新增,MongoDB 可以使用交叉索引來完成查詢。

對於指定複合條件的查詢,如果一個索引可以滿足查詢條件的一部分,而另一個索引可以滿足查詢條件的另一部分,則 MongoDB 可以使用兩個索引的交集來完成查詢。

使用複合索引還是使用交叉索引更有效取決於具體的查詢和系統。

覆蓋查詢

覆蓋查詢圖解

當查詢子句和查詢投影僅包含索引欄位時,MongoDB 可以直接從索引返回結果,而無需掃描任何文檔或載入文檔到記憶體。

這樣的覆蓋查詢非常有效,效率非常高。必要時還需要對不做查詢的欄位進行索引,以滿足覆蓋索引的要求。

如果對一個被覆蓋的查詢運行 explain,那麼結果中會有一個並不處於 FETCH 階段下的 IXSCAN 階段,並且在 executionStats 中,totalDocsExamined 的值是 0

查詢計劃

使用 explain 可以為查詢提供大量的信息,它是慢查詢的重要診斷工具之一。下述是執行結果示例:

{
    "queryPlanner": {
        "plannerVersion": 1,
        "namespace": "test.users",
        "indexFilterSet": false,
        "parsedQuery": {
            "age": {
                "$eq": 42
            }
        },
        "winningPlan": {
            "stage": "FETCH",
            "inputStage": {
                "stage": "IXSCAN",
                "keyPattern": {
                    "age": 1,
                    "username": 1
                },
                "indexName": "age_1_username_1",
                "isMultiKey": false,
                "multiKeyPaths": {
                    "age": [],
                    "username": []
                },
                "isUnique": false,
                "isSparse": false,
                "isPartial": false,
                "indexVersion": 2,
                "direction": "forward",
                "indexBounds": {
                    "age": [
                        "[42.0, 42.0]"
                    ],
                    "username": [
                        "[MinKey, MaxKey]"
                    ]
                }
            }
        },
        "rejectedPlans": []
    },
    "executionStats": {
        "executionSuccess": true,
        "nReturned": 8449,
        "executionTimeMillis": 15,
        "totalKeysExamined": 8449,
        "totalDocsExamined": 8449,
        "executionStages": {
            "stage": "FETCH",
            "nReturned": 8449,
            "executionTimeMillisEstimate": 10,
            "works": 8450,
            "advanced": 8449,
            "needTime": 0,
            "needYield": 0,
            "saveState": 66,
            "restoreState": 66,
            "isEOF": 1,
            "invalidates": 0,
            "docsExamined": 8449,
            "alreadyHasObj": 0,
            "inputStage": {
                "stage": "IXSCAN",
                "nReturned": 8449,
                "executionTimeMillisEstimate": 0,
                "works": 8450,
                "advanced": 8449,
                "needTime": 0,
                "needYield": 0,
                "saveState": 66,
                "restoreState": 66,
                "isEOF": 1,
                "invalidates": 0,
                "keyPattern": {
                    "age": 1,
                    "username": 1
                },
                "indexName": "age_1_username_1",
                "isMultiKey": false,
                "multiKeyPaths": {
                    "age": [],
                    "username": []
                },
                "isUnique": false,
                "isSparse": false,
                "isPartial": false,
                "indexVersion": 2,
                "direction": "forward",
                "indexBounds": {
                    "age": [
                        "[42.0, 42.0]"
                    ],
                    "username": [
                        "[MinKey, MaxKey]"
                    ]
                },
                "keysExamined": 8449,
                "seeks": 1,
                "dupsTested": 0,
                "dupsDropped": 0,
                "seenInvalidated": 0
            }
        }
    },
    "serverInfo": {
        "host": "eoinbrazil-laptop-osx",
        "port": 27017,
        "version": "4.0.12",
        "gitVersion": "5776e3cbf9e7afe86e6b29e22520ffb6766e95d4"
    },
    "ok": 1
}

explain 的結果當中,queryPlanner 描述了所有的查詢計劃,其中包括一個獲勝的查詢計劃 winningPlan 欄位,和一組失敗的查詢計劃 rejectedPlans 欄位。

executionStats 欄位包含了描述獲勝查詢計劃所執行的統計信息。

  • isMultiKey: 是否使用了多鍵索引
  • nReturned: 返回的文檔數量
  • totalDocsExamined: 按照索引指針在磁碟上查找實際文檔的次數
  • totalKeysExamined: 使用了索引時是查找過的索引條目數量,全表掃描時是檢查過的文檔數量
  • stage: 查詢階段,COLLSCAN 表示集合掃描,IXSCAN 表示索引掃描
  • needYield: 為了讓寫請求順利進行,本次查詢暫停的次數
  • executionTimeMillis: 所有查詢計劃花費的總毫秒數,不是所選的最優查詢計劃所耗費的時間
  • indexBounds: 描述了索引是如何被使用的,並給出了索引的遍歷範圍

優化的一個方向是,通過將 nReturnedtotalKeysExamined 作比較,兩個數值越是接近,表示索引的選擇性越高。

首發於翔仔的個人博客,點擊查看更多。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前提:最近公司下發通知,所有開發人員 必須 卸載 Navicat 資料庫可視化工具,不知道兄弟們有沒有在使用的,可能現在的反應跟我一樣,一臉懵逼,Navicat為什麼不能使用呢? 有事沒事找度娘,於是我。。 (簡單粗暴) 搜到了答案。。 原來如此,那就廢話不多說,這邊介紹 使用 DBeaver 來替 ...
  • 作用:隨時修改代碼 (在函數或類定義完成之後,再去修改函數的實現過程) """類似猴子補丁在函數定義好之後,再去更改他的行為"""import typesclass Valley: def func(self): return "等待宣告"def common(self): return "只有永不 ...
  • 1. 擴容方案剖析 1.1 擴容問題 在項目初期,我們部署了三個資料庫A、B、C,此時資料庫的規模可以滿足我們的業務需求。為了將數據做到平均分配,我們在Service服務層使用uid%3進行取模分片,從而將數據平均分配到三個資料庫中。 如圖所示: 後期隨著用戶量的增加,用戶產生的數據信息被源源不斷的 ...
  • 上文介紹了命令行方式來對文件進行加解密操作。本文將繼續在此基礎上,實現一個快速簡易的GUI界面方便操作,先上代碼看效果。 ...
  • aspnetcore上傳圖片也就是上傳文件有兩種方式,一種是通過form-data,一種是binary。 先介紹第一種form-data: 該方式需要顯示指定一個IFormFile類型,該組件會動態通過打開一個windows視窗選擇文件 及圖片。 postman演示如上,代碼如下: [HttpPos ...
  • FreeRtos操作系統 首先,應該介紹什麼是FreeRtos,他於單片機而言就是一個管理器,作為管理者管理嵌入式晶元中的任務,堆棧,中斷,隊列等等資源,對於操作系統而言,又分為實時操作系統和非實時操作系統,實時操作系統代表任務或者某個功能必須在指定的運行時間內完成,保證設備想要執行的功能能立即得到 ...
  • 摘要:先通過OPS確認節點狀態是否已經恢復,或登錄後臺執行cm_ctl query -Cv確認集群是否已經Normal。 本文分享自華為雲社區《【實例狀態】GaussDB CN服務異常》,作者:酷哥。 確認節點狀態 先通過OPS確認節點狀態是否已經恢復,或登錄後臺執行cm_ctl query -Cv ...
  • 一、前言 ChunJun(原FlinkX)是一個基於 Flink 提供易用、穩定、高效的批流統一的數據集成工具,既可以採集靜態的數據,比如 MySQL,HDFS 等,也可以採集實時變化的數據,比如 binlog,Kafka等。同時 ChunJun 也是一個支持原生 FlinkSql所有語法和特性的計 ...
一周排行
    -Advertisement-
    Play Games
  • 1.部署歷史 猿友們好,作為初來實習的我,已經遭受社會的“毒打”,所以請容許我在下麵環節適當吐槽,3Q! 傳統部署 ​ 回顧以往在伺服器部署webapi項目(非獨立發佈),dotnet環境、守護進程兩個逃都逃不掉,正常情況下還得來個nginx代理。不僅僅這仨,可能牽扯到yum或npm。node等都要 ...
  • 隨著技術的進步,跨平臺開發已經成為了標配,在此大背景下,ASP.NET Core也應運而生。本文主要基於ASP.NET Core+Element+Sql Server開發一個校園圖書管理系統為例,簡述基於MVC三層架構開發的常見知識點,前一篇文章,已經簡單介紹瞭如何搭建開發框架,和登錄功能實現,本篇... ...
  • 這道題只要會自定義cmp恰當地進行排序,其他部分沒有什麼大問題。 上代碼: 1 #include<bits/stdc++.h> 2 using namespace std; 3 int n,s,h1,h2,cnt; 4 struct apple{ 5 int height,ns;//height為蘋 ...
  • 這篇文章主要描述RPC的路由策略,包括為什麼需要請求隔離,為什麼不在註冊中心中實現請求隔離以及不同粒度的路由策略。 ...
  • 簡介: 中介者模式,屬於行為型的設計模式。用一個中介對象來封裝一系列的對象交互。中介者是各對象不需要顯式地相互引用,從而使其耦合鬆散,而且可以獨立地改變他們之間的交互。 適用場景: 如果平行對象間的依賴複雜,可以使用中介者解耦。 優點: 符合迪米特法則,減少成員間的依賴。 缺點: 不適用於系統出現對 ...
  • 【前置內容】Spring 學習筆記全系列傳送門: Spring學習筆記 - 第一章 - IoC(控制反轉)、IoC容器、Bean的實例化與生命周期、DI(依賴註入) Spring學習筆記 - 第二章 - 註解開發、配置管理第三方Bean、註解管理第三方Bean、Spring 整合 MyBatis 和 ...
  • 簡介: 享元模式,屬於結構型的設計模式。運用共用技術有效地支持大量細粒度的對象。 適用場景: 具有相同抽象但是細節不同的場景中。 優點: 把公共的部分分離為抽象,細節依賴於抽象,符合依賴倒轉原則。 缺點: 增加複雜性。 代碼: //用戶類 class User { private $name; fu ...
  • 這次設計一個通用的多位元組SPI介面模塊,特點如下: 可以設置為1-128位元組的SPI通信模塊 可以修改CPOL、CPHA來進行不同的通信模式 可以設置輸出的時鐘 狀態轉移圖和思路與多位元組串口發送模塊一樣,這裡就不給出了,具體可看該隨筆。 一、模塊代碼 1、需要的模塊 通用8位SPI介面模塊 `tim ...
  • AOP-03 7.AOP-切入表達式 7.1切入表達式的具體使用 1.切入表達式的作用: 通過表達式的方式定義一個或多個具體的連接點。 2.語法細節: (1)切入表達式的語法格式: execution([許可權修飾符] [返回值類型] [簡單類名/全類名] [方法名]([參數列表]) 若目標類、介面與 ...
  • 測試一、虛繼承與繼承的區別 1.1 單個繼承,不帶虛函數 1>class B size(8): 1> + 1> 0 | + (base class A) 1> 0 | | _ia //4B 1> | + 1> 4 | _ib //4B 有兩個int類型數據成員,占8B,基類邏輯存在前面 1.2、單個 ...