在本章中,您將瞭解Cassandra的數據模型以及Cassandra查詢語言(CQL)如何實現該數據模型。我們將展示CQL如何支持Cassandra的設計目標並查看一些一般行爲特徵。
對於來自關係世界的開發人員和管理員來說,Cassandra數據模型最初很難理解。一些術語,例如“鍵空間”,是全新的,有些術語,例如“列”,存在於兩個世界中,但含義略有不同。 CQL的語法在很多方面類似於SQL,但有一些重要的區別。對於那些熟悉NoSQL技術(如Dynamo或Bigtable)的人來說,它也可能令人困惑,因爲儘管Cassandra可能基於這些技術,但它自己的數據模型卻有很大不同。
因此,在本章中,我們從關係數據庫術語開始,介紹Cassandra對世界的看法。在此過程中,我們將更熟悉CQL並瞭解它如何實現此數據模型。
關係數據模型
在關係數據庫中,我們擁有數據庫本身,它是可能對應於單個應用程序的最外層容器。數據庫包含表。表具有名稱幷包含一個或多個列,這些列也具有名稱。當我們向表中添加數據時,我們爲每個定義的列指定一個值;如果我們沒有特定列的值,我們使用null。這個新條目向表中添加一行,如果我們知道行的唯一標識符(主鍵),或者使用表示行可能遇到的某些條件的SQL語句,我們稍後可以讀取該行。如果我們想要更新表中的值,我們可以更新所有行或只更新其中一些,具體取決於我們在SQL語句的“where”子句中使用的過濾器。
既然我們已經進行了這次審覈,那麼我們就可以從Cassandra的相似性和差異性的角度來看待它們。
Cassandra的數據模型
在本節中,我們將採用自下而上的方法來理解Cassandra的數據模型。
您可能想要使用的最簡單的數據存儲可能是數組或列表。它如圖4-1所示。
如果您持久保存此列表,則可以稍後查詢,但您必須檢查每個值以瞭解它所代表的內容,或者始終將每個值存儲在列表中的相同位置,然後從外部維護有關哪個單元格的文檔。 數組包含哪些值。 這意味着您可能必須提供空的佔位符值(空值),以便在您沒有可選屬性(例如傳真號碼或公寓號碼)的值時保留陣列的預定大小。 數組是一個明顯有用的數據結構,但在語義上並不豐富。
所以我們想在這個列表中添加第二個維度:匹配值的名稱。 我們將爲每個單元格命名,現在我們有了一個地圖結構,如圖4-2所示。
這是一個改進,因爲我們可以知道我們的值的名稱。因此,如果我們確定我們的地圖將保存用戶信息,我們可以使用列名,如first_name,last_name,phone,email等。這是一個更豐富的結構。
但到目前爲止我們構建的結構只有在我們有一個給定實體的實例時纔有效,例如單個人,用戶,酒店或推文。如果我們想要存儲具有相同結構的多個實體,它不會給我們太多,這當然是我們想要做的。沒有什麼可以統一一些名稱/值對的集合,也沒有辦法重複相同的列名。因此,我們需要一些能夠將一些列值組合在一個明顯可尋址的組中的東西。我們需要一個鍵來引用一組應該作爲集合處理的列。我們需要行。然後,如果我們得到一行,我們可以同時獲得單個實體的所有名稱/值對,或者只獲取我們感興趣的名稱的值。我們可以調用這些名稱/值對列。我們可以調用每個包含一些列行的獨立實體。並且每行的唯一標識符可以稱爲行鍵或主鍵。圖4-3顯示了一個簡單行的內容:主鍵,它本身是一列或多列,以及其他列。
Cassandra將表定義爲關聯相似數據的邏輯分區。例如,我們可能有一個用戶表,一個酒店表,一個地址簿表等。通過這種方式,Cassandra表類似於關係世界中的表。
現在,每次存儲新實體時,我們都不需要爲每列存儲值。也許我們不知道給定實體的每列的值。例如,有些人有第二個電話號碼,有些人沒有,在Cassandra支持的在線表格中,可能有一些字段是可選的,有些字段是必需的。沒關係。而不是爲那些我們不知道的值存儲null,這會浪費空間,我們根本不會爲該行存儲該列。所以現在我們有一個稀疏的多維數組結構,如圖4-4所示。
在傳統關係數據庫中設計表時,通常會處理“實體”或描述特定名詞(酒店,用戶,產品等)的屬性集。沒有考慮行本身的大小,因爲一旦你確定了你的表所代表的名詞,行大小就不可協商了。但是,當您使用Cassandra時,實際上您決定要確定行的大小:它們可以是寬或粗,取決於行所包含的列數。
寬行表示具有批次(可能是數萬甚至數百萬)列的行。通常,與這麼多列一起使用的行數較少。相反,你可以更接近關係模型,在那裏定義較少數量的列並使用許多不同的行 - 這就是瘦模型。我們已經在圖4-4中看到了一個瘦小的模型。
Cassandra使用稱爲複合鍵(或複合鍵)的特殊主鍵來表示寬行,也稱爲分區。 組合鍵由分區鍵和一組可選的聚類列組成。 分區鍵用於確定存儲行的節點,並且本身可以包含多個列。 聚類列用於控制數據在分區中的存儲方式。 Cassandra還支持另一個稱爲靜態列的構造,該構造用於存儲不屬於主鍵但由分區中的每一行共享的數據。
圖4-5顯示了每個分區如何由分區鍵唯一標識,以及如何使用羣集鍵唯一標識分區中的行。
在本章中,我們將關注由單個列組成的簡單主鍵。在這些情況下,主鍵和分區鍵是相同的,因爲我們沒有聚類列。我們將在第5章中研究更復雜的主鍵。
綜上所述,我們有基本的Cassandra數據結構:
列,是名稱/值對
該行,是主鍵引用的列的容器
該表是行的容器
鍵空間,是表的容器
集羣,它是跨越一個或多個節點的密鑰空間的容器
這是查看Cassandra數據模型的自下而上的方法。現在我們已經瞭解了基本術語,讓我們更詳細地研究每個結構。
集羣
如前所述,Cassandra數據庫專門設計爲分佈在多個一起運行的計算機上,這些計算機作爲單個實例出現給最終用戶。因此,Cassandra中最外層的結構是集羣,有時稱爲環,因爲Cassandra通過將數據排列成環來將數據分配給集羣中的節點。
Keyspaces
集羣是密鑰空間的容器。密鑰空間是Cassandra中數據的最外層容器,與關係數據庫緊密對應。與數據庫是關係模型中表的容器的方式相同,鍵空間是Cassandra數據模型中表的容器。與關係數據庫一樣,鍵空間具有一個名稱和一組定義鍵空間範圍行爲的屬性。
因爲我們目前專注於數據模型,所以我們將留下有關設置和配置羣集和密鑰空間的問題,直到稍後。 我們將在第7章中研究這些主題。
表
表是有序行集合的容器,每個行本身都是有序的列集合。 排序由列確定,列被標識爲鍵。 我們很快就會看到Cassandra如何使用主鍵之外的其他鍵。
將數據寫入Cassandra中的表時,可以指定一列或多列的值。 該值集合稱爲行。 您指定的值中至少有一個必須是主鍵,用作該行的唯一標識符。
讓我們回到我們在前一章中創建的用戶表。 記住我們如何編寫一行數據,然後使用cqlsh中的SELECT命令讀取它:
cqlsh:my_keyspace> SELECT * FROM user WHERE first_name='Bill';
first_name | last_name
------------+-----------
Bill | Nguyen
(1 rows)
你會在最後一行注意到shell告訴我們返回了一行。 事實證明,這是由first_name“Bill”標識的行。 這是標識此行的主鍵。
這是一個重要的細節–CQL中的SELECT,INSERT,UPDATE和DELETE命令都按行進行操作。
如前所述,當我們向表中添加新行時,我們不需要爲每列包含值。 讓我們使用ALTER TABLE命令使用我們的用戶表對此進行測試,然後使用DESCRIBE TABLE命令查看結果:
cqlsh:my_keyspace> ALTER TABLE user ADD title text;
cqlsh:my_keyspace> DESCRIBE TABLE user;
CREATE TABLE my_keyspace.user (
first_name text PRIMARY KEY,
last_name text,
title text
) ...
我們看到標題欄已添加。 請注意,我們縮短了輸出以省略各種表設置。 您將在第7章中瞭解有關這些設置以及如何配置它們的更多信息。
現在,讓我們寫幾行,爲每個行填充不同的列,並查看結果:
cqlsh:my_keyspace> INSERT INTO user (first_name, last_name, title)
VALUES ('Bill', 'Nguyen', 'Mr.');
cqlsh:my_keyspace> INSERT INTO user (first_name, last_name) VALUES
('Mary', 'Rodriguez');
cqlsh:my_keyspace> SELECT * FROM user;
first_name | last_name | title
------------+-----------+-------
Mary | Rodriguez | null
Bill | Nguyen | Mr.
(2 rows)
現在我們已經更多地瞭解了表的結構並完成了一些數據建模,讓我們深入研究一下列。
列
列是Cassandra數據模型中最基本的數據結構單元。到目前爲止,我們已經看到一列包含名稱和值。在定義列時,我們將每個值約束爲特定類型。我們想深入研究每列可用的各種類型,但首先讓我們看一下我們尚未討論過的列的其他一些屬性:時間戳和生存時間。這些屬性是瞭解Cassandra如何利用時間保持數據最新的關鍵。
時間戳
每次將數據寫入Cassandra時,都會爲更新的每個列值生成時間戳。在內部,Cassandra使用這些時間戳來解決對相同值進行的任何衝突更改。通常,最後一個時間戳獲勝。
讓我們通過將writetime()函數添加到SELECT命令來查看爲以前的寫入生成的時間戳。我們將在lastname列上執行此操作,併爲上下文包含其他幾個值:
cqlsh:my_keyspace> SELECT first_name, last_name,
writetime(last_name) FROM user;
first_name | last_name | writetime(last_name)
------------+-----------+----------------------
Mary | Rodriguez | 1434591198790252
Bill | Nguyen | 1434591198798235
(2 rows)
我們可能期望如果我們在first_name上請求時間戳,我們會得到類似的結果。 但是,事實證明我們不允許在主鍵列上詢問時間戳:
cqlsh:my_keyspace> SELECT WRITETIME(first_name) FROM user;
InvalidRequest: code=2200 [Invalid query] message="Cannot use
selection function writeTime on PRIMARY KEY part first_name"
Cassandra還允許我們指定執行寫入時要使用的時間戳。 爲此,我們將首次使用CQL UPDATE命令。 我們將使用可選的USING TIMESTAMP選項手動設置時間戳(請注意,時間戳必須晚於SELECT命令中的時間戳,否則將忽略UPDATE):
cqlsh:my_keyspace> UPDATE user USING TIMESTAMP 1434373756626000
SET last_name = 'Boateng' WHERE first_name = 'Mary' ;
cqlsh:my_keyspace> SELECT first_name, last_name,
WRITETIME(last_name) FROM user WHERE first_name = 'Mary';
first_name | last_name | writetime(last_name)
------------+-------------+---------------------
Mary | Boateng | 1434373756626000
(1 rows)
此語句具有將姓氏列添加到由主鍵“Mary”標識的行,並將時間戳設置爲我們提供的值的效果。
寫入不需要設置時間戳。此功能通常用於寫入,其中擔心某些寫入可能導致新數據被陳舊數據覆蓋。這是高級行爲,應謹慎使用。
目前還沒有辦法將writetime()生成的時間戳轉換爲cqlsh中更友好的格式。
生存時間(TTL)
Cassandra提供的一個非常強大的功能是能夠使不再需要的數據到期。此過期非常靈活,可以在各個列值的級別上運行。生存時間(或TTL)是Cassandra爲每個列值存儲的值,用於指示保留值的時間。
TTL值默認爲null,表示寫入的數據不會過期。讓我們通過將CTL()函數添加到cqlsh中的SELECT命令來顯示這一點,以查看Mary的姓氏的TTL值:
cqlsh:my_keyspace> SELECT first_name, last_name, TTL(last_name)
FROM user WHERE first_name = 'Mary';
first_name | last_name | ttl(last_name)
------------+-----------+----------------
Mary | Boateng | null
(1 rows)
現在讓我們通過在UPDATE命令中添加USING TTL選項,將姓氏列上的TTL設置爲一小時(3,600秒):
cqlsh:my_keyspace> UPDATE user USING TTL 3600 SET last_name =
'McDonald' WHERE first_name = 'Mary' ;
cqlsh:my_keyspace> SELECT first_name, last_name, TTL(last_name)
FROM user WHERE first_name = 'Mary';
first_name | last_name | ttl(last_name)
------------+-------------+---------------
Mary | McDonald | 3588
(1 rows)
如您所見,時鐘已經在倒數我們的TTL,反映了輸入第二個命令所需的幾秒鐘。如果我們在一小時內再次運行此命令,則Mary的last_name將設置爲null。我們還可以使用相同的USING TTL選項在INSERTS上設置TTL。
請記住,TTL存儲在每列級別。目前沒有直接在行級設置TTL的機制。與時間戳一樣,無法獲取或設置主鍵列的TTL值,並且只有在爲列提供值時才能爲列設置TTL。
如果我們想在整行中設置TTL,我們必須在INSERT或UPDATE命令中爲每個非主鍵列提供一個值。
CQL類型
現在我們已經深入探討了Cassandra如何表示包含基於時間的元數據的列,讓我們看一下我們可用於我們的值的各種類型。
正如我們在目前的探索中看到的那樣,我們表中的每一列都是指定的類型。到目前爲止,我們只使用了varchar類型,但是在CQL中我們還有很多其他選項,所以讓我們來探索它們。
CQL支持一組靈活的數據類型,包括簡單的字符和數字類型,集合和用戶定義的類型。我們將描述這些數據類型,並提供一些示例,說明如何使用它們來幫助您學習如何爲數據模型做出正確的選擇。
數字數據類型
CQL支持您期望的數字類型,包括整數和浮點數。 這些類型與Java和其他語言中的標準類型類似:
- INT
- 32位有符號整數(如Java中所示)
- BIGINT
- 64位有符號長整數(相當於Java long)
- SMALLINT
- 一個16位有符號整數(相當於Java短)
- TINYINT
- 一個8位有符號整數(如Java中所示)
- varint
- 變量精度有符號整數(相當於java.math.BigInteger)
- 浮動
- 32位IEEE-754浮點(如Java中所示)
- 雙
- 64位IEEE-754浮點(如Java中所示)
- 十進制
- 變精度十進制(相當於java.math.BigDecimal)
在Cassandra 2.2版本中添加了smallint和tinyint類型。
雖然枚舉類型在許多語言中很常見,但在CQL中沒有直接的等價物。 通常的做法是將枚舉值存儲爲字符串。 例如,使用Enum.name()方法將枚舉值轉換爲String以便將Cassandra作爲文本寫入,並使用Enum.valueOf()方法將文本轉換回枚舉值。
文本數據類型
CQL提供了兩種用於表示文本的數據類型,其中一種我們已經對已經(文本)進行了相當多的使用:
- text,varchar
- UTF-8字符串的同義詞
- ASCII
- ASCII字符串
UTF-8是最新且廣泛使用的文本標準並支持國際化,因此我們建議在爲新數據構建表時使用ascii上的文本。 如果您要處理ASCII格式的舊數據,則ascii類型最有用。
默認情況下,cqlsh使用反斜槓轉義輸出控件和其他不可打印的字符。 您可以通過在運行該工具之前通過’$ LANG’環境變量設置語言環境來控制cqlsh如何顯示非ASCII字符。 有關更多信息,請參閱cqlsh命令HELP TEXT_OUTPUT。
時間和身份數據類型
行和分區等數據元素的標識在任何數據模型中都很重要,以便能夠訪問數據。 Cassandra提供了幾種在定義唯一分區鍵時非常有用的類型。 我們花點時間(雙關語)來深入研究這些:
-
時間戳
雖然我們之前已經注意到每列都有一個時間戳,指示上次修改的時間,但您也可以使用時間戳作爲列本身的值。 時間可以編碼爲64位有符號整數,但使用幾種支持的ISO 8601日期格式之一輸入時間戳通常更有用。 例如:
2015-06-15 20:05-0700
2015-06-15 20:05:07-0700
2015-06-15 20:05:07.013-0700
2015-06-15T20:05-0700
2015-06-15T20:05:07-0700
2015-06-15T20:05:07.013+-0700
最佳做法是始終提供時區而不是依賴於操作系統時區配置。
-
date,time
通過Cassandra 2.1發佈的只有時間戳類型來表示時間,包括日期和時間。 2.2版本引入了允許這些日期和時間獨立表示的日期和時間類型;也就是說,沒有時間的日期,以及沒有參考特定日期的時間。與時間戳一樣,這些類型支持ISO 8601格式。
雖然Java 8中提供了新的java.time類型,但日期類型映射到Cassandra中的自定義類型,以保持與舊JDK的兼容性。時間類型映射到Java long,表示自午夜以來的納秒數。
-
UUID
通用唯一標識符(UUID)是128位值,其中位符合幾種類型中的一種,其中最常用的類型稱爲類型1和類型4.CQL uuid類型是類型4 UUID,其中完全基於隨機數。 UUID通常表示爲以十六進制數字劃分的短劃線序列。例如:
1a6300ca-0572-4736-a393-c0b7229e193e
uuid類型通常用作代理鍵,可以單獨使用,也可以與其他值組合使用。
由於UUID的長度有限,因此並不能絕對保證它們是唯一的。但是,大多數操作系統和編程語言都提供了生成提供足夠唯一性的ID的實用程序,而cqlsh也是如此。您可以通過uuid()函數獲取Type 4 UUID值,並在INSERT或UPDATE中使用此值。
-
timeuuid
這是Type 1 UUID,它基於計算機的MAC地址,系統時間和用於防止重複的序列號。此類型經常用作無衝突時間戳。 cqlsh提供了幾個與timeuuid類型交互的便利函數:now(),dateOf()和unixTimestampOf()。
這些便利功能的可用性是timeuuid比uuid更頻繁使用的一個原因。
在前面的示例的基礎上,我們可能會確定我們要爲每個用戶分配一個唯一的ID,因爲first_name可能不是我們用戶表的唯一鍵。畢竟,我們很可能會在某些時候遇到具有相同名字的用戶。如果我們從頭開始,我們可能已經選擇將此標識符作爲主鍵,但是現在我們將其添加爲另一列。
創建表後,無法修改主鍵,因爲它控制數據在羣集中的分佈方式,更重要的是,它如何存儲在磁盤上。
讓我們使用uuid添加標識符:
cqlsh:my_keyspace> ALTER TABLE user ADD id uuid;
接下來,我們將使用uuid()函數爲Mary插入一個ID,然後查看結果:
cqlsh:my_keyspace> UPDATE user SET id = uuid() WHERE first_name =
'Mary';
cqlsh:my_keyspace> SELECT first_name, id FROM user WHERE
first_name = 'Mary';
first_name | id
------------+--------------------------------------
Mary | e43abc5d-6650-4d13-867a-70cbad7feda9
(1 rows)
請注意,id是UUID格式。
現在我們有了一個更強大的表設計,當我們瞭解更多類型時,我們可以擴展更多列。
其他簡單數據類型
CQL提供了其他幾種簡單的數據類型,這些數據類型不能很好地歸入我們已經看過的類別之一:
-
布爾
- 這是一個簡單的真/假值。 cqlsh在接受這些值時不區分大小寫,但輸出True或False。
-
BLOB
- 二進制大對象(blob)是任意字節數組的口語計算術語。 CQL blob類型對於存儲媒體或其他二進制文件類型很有用。 Cassandra不驗證或檢查blob中的字節。 CQL將數據表示爲十六進制數字 - 例如,0x00000ab83cf0。 如果要將任意文本數據編碼到blob中,可以使用textAsBlob()函數來指定條目的值。 有關更多信息,請參閱cqlsh幫助函數HELP BLOB_INPUT。
-
INET
-
此類型表示IPv4或IPv6 Internet地址。 cqlsh接受用於定義IPv4地址的任何合法格式,包括包含十進制,八進制或十六進制值的點線或非點線表示。但是,這些值使用cqlsh輸出中的點分十進制格式表示 - 例如,192.0.2.235。
-
IPv6地址表示爲由四個十六進制數字組成的八組,以冒號分隔 - 例如,2001:0db8:85a3:0000:0000:8a2e:0370:7334。 IPv6規範允許摺疊連續的零十六進制值,因此當使用SELECT:2001:db8:85a3:a :: 8a2e:370:7334讀取時,前面的值呈現如下。
-
-
計數器
- 計數器數據類型提供64位有符號整數,其值不能直接設置,而只能遞增或遞減。 Cassandra是爲數不多的跨數據中心提供無競爭增量的數據庫之一。計數器經常用於跟蹤統計數據,例如頁面查看次數,推文數,日誌消息等。計數器類型有一些特殊限制。它不能用作主鍵的一部分。如果使用計數器,則除主鍵列之外的所有列都必須是計數器。
請記住:增量和減量運算符不是冪等的。沒有直接復位計數器的操作,但您可以通過讀取計數器值並按該值遞減來近似復位。不幸的是,這並不能保證完美地工作,因爲在閱讀和寫作之間的其他地方可能已經改變了計數器。
集合
假設我們想擴展用戶表以支持多個電子郵件地址。一種方法是創建其他列,如email2,email3等。雖然這是一種可行的方法,但它不能很好地擴展,並且可能會導致大量的返工。將電子郵件地址作爲一個組或“集合”處理起來要簡單得多.CQL提供了三種集合類型來幫助我們解決這些問題:集合,列表和映射。我們現在來看看它們中的每一個:
-
set
- set數據類型存儲元素集合。元素是無序的,但cqlsh按排序順序返回元素。例如,文本值按字母順序返回。集合可以包含我們之前評論過的簡單類型以及用戶定義的類型(我們將暫時討論)甚至其他集合。使用set的一個優點是無需先讀取內容即可插入其他項目。
讓我們修改我們的用戶表以添加一組電子郵件地址:
cqlsh:my_keyspace> ALTER TABLE user ADD emails set<text>;
然後我們將爲Mary添加一個電子郵件地址並檢查它是否已成功添加:
cqlsh:my_keyspace> UPDATE user SET emails = {
'[email protected]' } WHERE first_name = 'Mary';
cqlsh:my_keyspace> SELECT emails FROM user WHERE first_name =
'Mary';
emails
----------------------
{'[email protected]'}
(1 rows)
請注意,在添加第一個電子郵件地址時,我們替換了該集合的先前內容,在本例中爲null。 我們可以稍後添加另一個電子郵件地址,而不使用連接替換整個集合:
cqlsh:my_keyspace> UPDATE user SET emails = emails + {
'[email protected]' } WHERE first_name = 'Mary';
cqlsh:my_keyspace> SELECT emails FROM user WHERE first_name =
'Mary';
emails
---------------------------------------------------
{'[email protected]', '[email protected]'}
(1 rows)
我們還可以使用減法運算符清除集合中的項目:SET emails = emails - {‘[email protected]’}。
或者,我們可以使用空集符號清除整個集合:SET emails = {}。
-
list
- 列表數據類型包含有序的元素列表。 默認情況下,值按插入順序存儲。 讓我們修改我們的用戶表以添加電話號碼列表:
cqlsh:my_keyspace> ALTER TABLE user ADD
phone_numbers list<text>;
然後我們將爲Mary添加一個電話號碼並檢查它是否已成功添加:
cqlsh:my_keyspace> UPDATE user SET phone_numbers = [
'1-800-999-9999' ] WHERE first_name = 'Mary';
cqlsh:my_keyspace> SELECT phone_numbers FROM user WHERE
first_name = 'Mary';
phone_numbers
--------------------
['1-800-999-9999']
(1 rows)
讓我們通過附加它來添加第二個數字:
cqlsh:my_keyspace> UPDATE user SET phone_numbers =
phone_numbers + [ '480-111-1111' ] WHERE first_name = 'Mary';
cqlsh:my_keyspace> SELECT phone_numbers FROM user WHERE
first_name = 'Mary';
phone_numbers
------------------------------------
['1-800-999-9999', '480-111-1111']
(1 rows)
我們現在添加的第二個數字顯示在列表的末尾。
我們也可以通過顛倒我們的值的順序將數字添加到列表的前面:SET phone_numbers = [‘4801234567’] + phone_numbers。
當我們通過索引引用它時,我們可以替換列表中的單個項目:
cqlsh:my_keyspace> UPDATE user SET phone_numbers[1] =
'480-111-1111' WHERE first_name = 'Mary';
與集合一樣,我們也可以使用減法運算符來刪除與指定值匹配的項目:
cqlsh:my_keyspace> UPDATE user SET phone_numbers =
phone_numbers - [ '480-111-1111' ] WHERE first_name = 'Mary';
最後,我們可以使用索引直接刪除特定項目:
cqlsh:my_keyspace> DELETE phone_numbers[0] from user WHERE
first_name = 'Mary';
-
map
地圖數據類型包含鍵/值對的集合。 鍵和值可以是除計數器之外的任何類型。 讓我們通過使用地圖來存儲有關用戶登錄的信息來嘗試這一點。 我們將創建一個列來跟蹤以秒爲單位的登錄會話時間,並以timeuuid爲關鍵:
cqlsh:my_keyspace> ALTER TABLE user ADD
login_sessions map<timeuuid, int>;
然後我們將爲Mary添加幾個登錄會話並查看結果:
cqlsh:my_keyspace> UPDATE user SET login_sessions =
{ now(): 13, now(): 18} WHERE first_name = 'Mary';
cqlsh:my_keyspace> SELECT login_sessions FROM user WHERE
first_name = 'Mary';
login_sessions
-----------------------------------------------
{6061b850-14f8-11e5-899a-a9fac1d00bce: 13,
6061b851-14f8-11e5-899a-a9fac1d00bce: 18}
(1 rows)
我們還可以使用其鍵引用地圖中的單個項目。
在我們需要在單個列中存儲可變數量的元素的情況下,集合類型非常有用。
用戶定義的類型
現在我們可能決定需要跟蹤用戶的物理地址。 我們可以使用單個文本列來存儲這些值,但這會給應用程序解析地址的各個組件帶來負擔。 如果我們能夠定義一個存儲地址的結構以保持不同組件的完整性,那就更好了。
幸運的是,Cassandra爲我們提供了一種定義自己類型的方法。 然後,我們可以創建這些用戶定義類型(UDT)的列。 讓我們創建自己的地址類型,在命令中插入一些換行符以提高可讀性:
cqlsh:my_keyspace> CREATE TYPE address (
... street text,
... city text,
... state text,
... zip_code int);
UDT的範圍由定義它的鍵空間確定。 我們可以編寫CREATE TYPE my_keyspace.address。 如果運行命令DESCRIBE KEYSPACE my_keyspace,您將看到地址類型是鍵空間定義的一部分。
現在我們已經定義了地址類型,我們將嘗試在用戶表中使用它,但是我們立即遇到了一個問題:
cqlsh:my_keyspace> ALTER TABLE user ADD
addresses map<text, address>;
InvalidRequest: code=2200 [Invalid query] message="Non-frozen
collections are not allowed inside collections: map<text,
address>"
這裏發生了什麼?事實證明,用戶定義的數據類型被視爲集合,因爲它的實現類似於集合,列表或映射。
2.2之前的Cassandra版本不完全支持集合的嵌套。具體而言,尚不支持訪問嵌套集合的各個屬性的能力,因爲嵌套集合由實現序列化爲單個對象。
凍結是Cassandra社區作爲前向兼容機制引入的一個概念。現在,您可以將集合標記爲已凍結,從而將集合嵌套在另一個集合中。將來,當完全支持嵌套集合時,將有一種“解凍”嵌套集合的機制,允許訪問各個屬性。
如果凍結,您還可以將集合用作主鍵。
既然我們已經花了很長時間來討論凍結和嵌套表,那麼讓我們回到修改我們的表,這次將地址標記爲凍結:
cqlsh:my_keyspace> ALTER TABLE user ADD addresses map<text,
frozen<address>>;
現在讓我們爲Mary添加一個家庭住址:
cqlsh:my_keyspace> UPDATE user SET addresses = addresses +
{'home': { street: '7712 E. Broadway', city: 'Tucson',
state: 'AZ', zip_code: 85715} } WHERE first_name = 'Mary';
現在我們已經完成了各種類型的學習,讓我們退後一步,通過描述my_keyspace來查看我們目前創建的表格:
cqlsh:my_keyspace> DESCRIBE KEYSPACE my_keyspace ;
CREATE KEYSPACE my_keyspace WITH replication = {'class':
'SimpleStrategy', 'replication_factor': '1'} AND
durable_writes = true;
CREATE TYPE my_keyspace.address (
street text,
city text,
state text,
zip_code int
);
CREATE TABLE my_keyspace.user (
first_name text PRIMARY KEY,
addresses map<text, frozen<address>>,
emails set<text>,
id uuid,
last_name text,
login_sessions map<timeuuid, int>,
phone_numbers list<text>,
title text
) WITH bloom_filter_fp_chance = 0.01
AND caching = '{'keys':'ALL', 'rows_per_partition':'NONE'}'
AND comment = ''
AND compaction = {'min_threshold': '4', 'class':
'org.apache.cassandra.db.compaction.
SizeTieredCompactionStrategy', 'max_threshold': '32'}
AND compression = {'sstable_compression':
'org.apache.cassandra.io.compress.LZ4Compressor'}
AND dclocal_read_repair_chance = 0.1
AND default_time_to_live = 0
AND gc_grace_seconds = 864000
AND max_index_interval = 2048
AND memtable_flush_period_in_ms = 0
AND min_index_interval = 128
AND read_repair_chance = 0.0
AND speculative_retry = '99.0PERCENTILE';
二級索引
如果您嘗試查詢Cassandra表中不屬於主鍵的列,您很快就會意識到這是不允許的。 例如,請考慮上一章中的用戶表,該表使用first_name作爲主鍵。 嘗試按last_name查詢會產生以下輸出:
cqlsh:my_keyspace> SELECT * FROM user WHERE last_name = 'Nguyen';
InvalidRequest: code=2200 [Invalid query] message="No supported
secondary index found for the non primary key columns restrictions"
正如錯誤消息指示我們,我們需要爲last_name列創建二級索引。 輔助索引是不屬於主鍵的列的索引:
cqlsh:my_keyspace> CREATE INDEX ON user ( last_name );
我們還可以使用語法CREATE INDEX ON爲索引提供可選名稱…如果沒有指定名稱,cqlsh會根據表格
_ 自動創建名稱_idx。 例如,我們可以使用DESCRIBE KEYSPACE學習我們剛剛創建的索引的名稱:cqlsh:my_keyspace> DESCRIBE KEYSPACE;
...
CREATE INDEX user_last_name_idx ON my_keyspace.user (last_name);
現在我們已經創建了索引,我們的查詢將按預期工作:
cqlsh:my_keyspace> SELECT * FROM user WHERE last_name = 'Nguyen';
first_name | last_name
------------+-----------
Bill | Nguyen
(1 rows)
我們不僅僅限於僅基於簡單類型列的索引。 也可以創建基於集合中的值的索引。 例如,我們可能希望能夠根據用戶地址,電子郵件或電話號碼進行搜索,我們分別使用map,set和list實現:
cqlsh:my_keyspace> CREATE INDEX ON user ( addresses );
cqlsh:my_keyspace> CREATE INDEX ON user ( emails );
cqlsh:my_keyspace> CREATE INDEX ON user ( phone_numbers );
請注意,對於地圖,我們可以選擇索引鍵(通過語法KEYS(地址))或值(默認值)或兩者(在Cassandra 2.2或更高版本中)。
最後,我們可以使用DROP INDEX命令刪除索引:
cqlsh:my_keyspace> DROP INDEX user_last_name_idx;
由於Cassandra跨多個節點分區數據,因此每個節點必須根據其擁有的分區中存儲的數據維護自己的二級索引副本。出於這個原因,涉及二級索引的查詢通常涉及更多節點,這使得它們顯得更加昂貴。
對於幾種特定情況,不建議使用二級索引:
具有高基數的列。例如,對user.addresses列進行索引可能非常昂貴,因爲絕大多數地址都是唯一的。
數據基數非常低的列。例如,對user.title列進行索引以支持對用戶表中每個“Mrs.”的查詢沒有多大意義,因爲這會導致索引中存在大量行。
經常更新或刪除的列。如果刪除的數據(邏輯刪除)的數量比壓縮過程可以處理的更快,那麼構建在這些列上的索引可能會產生錯誤。
爲了獲得最佳讀取性能,非規範化表格設計或物化視圖通常優於使用二級索引。我們將在第5章中瞭解有關這些內容的更多信息。但是,二級索引可以是支持初始數據模型設計中未考慮的查詢的有用方法。
Cassandra 3.4版本包括二級索引的替代實現,稱爲SSTable Attached Secondary Index(SASI)。 SASI由Apple開發,並作爲Cassandra二級索引API的開源實現發佈。顧名思義,SASI索引是作爲每個SSTable文件的一部分計算和存儲的,與原始的Cassandra實現不同,後者將索引存儲在單獨的“隱藏”表中。
SASI實現與傳統的二級索引一起存在,您可以使用CQL CREATE CUSTOM INDEX命令創建SASI索引:
CREATE CUSTOM INDEX user_last_name_sasi_idx ON user (last_name)
USING 'org.apache.cassandra.index.sasi.SASIIndex';
SASI索引確實提供了超越傳統二級索引實現的功能,例如能夠對索引列執行不等式(大於或小於)搜索。您還可以使用新的CQL LIKE關鍵字對索引列進行文本搜索。例如,您可以使用以下查詢來查找姓氏以“N”開頭的用戶:
SELECT * FROM user WHERE last_name LIKE'N%';
雖然SASI索引通過消除從其他表讀取的需要確實比傳統索引執行得更好,但它們仍然需要從非規範化設計中讀取更多節點。
總結
在本章中,我們快速瀏覽了Cassandra的集羣,鍵空間,表,鍵,行和列的數據模型。 在這個過程中,我們學習了很多CQL語法,並獲得了使用cqlsh中的表和列的更多經驗。 如果您對深入瞭解CQL感興趣,可以閱讀完整的語言規範。