CockroachDB学习笔记——[译]CockroachDB中的SQL:映射表中数据到键值存储
- 原文标题:SQL in CockroachDB: Mapping Table Data to Key-Value Storage
- 原文链接:https://www.cockroachlabs.com/blog/sql-in-cockroachdb-mapping-table-data-to-key-value-storage/
- 原作者:Peter Mattis , Tamir Duberstein
- 原文日期:Sep 16, 2015
- 译:zifeiy
CockroachDB中的SQL:映射表中数据到键值存储
SQL?我觉得CockroachDB是一个键值存储?!?
在过去,我们将CockroachDB描述成一个分布式的、事物一致性的、键-值存储数据库。
我们知道我们想要提供的并不仅仅是一个键-值模型的API,我们想要设计的是一个更高级的结构化数据API,让他能够支持数据库中的表(table)和索引(index)。
除了支持这样的丰富结构外,我们还期望最终支持SQL来操纵和访问这种结构化数据。
经过一番绞尽脑汁之后,我们排除了所有使用SQL的不可能性,并以SQL作为我们结构化数据层的核心,全速前进。
在一个SQL中有很多组建需要设计,仅仅据一些例子:查询解析(query parsing),查询分析(query analysis),查询规划(query planning),查询执行(query execution),事务(transactions),持久存储(persistent storage)。
CockroachDB的SQL系统建立在内部的CockroachDB 键-值存储之上,并利用整体排序的键值映射来存储所有SQL表数据和索引。
本文将重点讨论CockroachDB在SQL数据和键-值存储中的映射关系,并且展示这种映射如何磅数实现SQL功能。
未来的文章将讨论查询分析、规划和执行。
一个SQL表(table)是由一系列的行(row)组成的,每一行包含若干列(column)。
每一列都有它关联的类型(bool, int, float, string, bytes)。
表还具有关联索引,允许从表中高效地检索行的范围。
这听起来根本不像是一个将字符串映射到字符串的键-值API呀。如何将SQL表映射到K-V(键-值)存储呢?
首先,一个入门:CockroachDB内部的键-值 API支持大量的操作,但是在这篇文章中我们只需要知道其中的一部分:
-
ConditionalPut(key, value, expected-value)
- 如果expect-value对应的条件成立,则我们将value的值赋给key。 -
Scan(start-key, end-key)
- 在区间[start-key,end-key)对应的区间(左闭右开区间)内检索所有的键。
在CockroachDB中,键和值都可以包含不限制字节数的字符串。
Ok!让我们继续!
键编码(Key Encoding)
将SQL表中的数据到键和值的基本问题是将有类型之分的列数据编码为字符串。
比如,给定一组数值<1, 2.3, "four”>
,我们会将其编码为一个字符串,而这个字符串看上去像是这样的:
/1/2.3/"four”
我们使用反斜杠作为值之间的视觉分隔符,尽管这仅仅是出于可读性的目的。
我们可以把一篇完整的博客文章献给这些编码。(意思是:说到编码我们可以将一大堆咯,大家就先按照我这么说的理解有可以了)
为了简单起见,这里只讨论了它们的性质,而不是它们的实现。
被编码的键们是排序的,因此键的每一个字段都被认为是分开的:
/1/2.3/"four”
/2/3.1/"six”
/10/4.6/"seven”
如果你按照原始的方法天真的为这些字符串排序,你会发现/10/...
是排在/2/...
之前的。
编码工作可以变得更神奇一点如果你之前没有碰到过类似情况。
如果你对编码的细节感兴趣可以在util/encoding中查看{Encode,Decode}{Varint,Float,Bytes,Null}
。
(译者注:原链接https://github.com/cockroachdb/cockroach/tree/master/util/encoding已失效,译者找到了新的链接https://github.com/cockroachdb/cockroach/tree/master/pkg/util/encoding)
有了这个编码工具,我们就可以窥视SQL表数据的编码。
在CockroachDB中,每个表在创建时都有一个唯一的64位整数ID分配给它。
此表ID用作与该表关联的所有键的前缀。
现在让我们考虑如下的表和数据:
CREATE TABLE test (
key INT PRIMARY KEY,
floatVal FLOAT,
stringVal STRING
)
INSERT INTO test VALUES (10, 4.5, "hello”)
CockroachDB中的每一张表都必须有一个主键。
主键由一个或多个列组成;在上面的测试表中,它由单个列组成。
CockroachDB将每个非主键列存储在由主键作为前缀并且由列名作为后缀的单独密钥中。
因此,行<10, 4.5, "hello">
将以如下形式存储在我们的test
表中:
Key | Value |
---|---|
/test/10/floatVal |
4.5 |
/test/10/stringVal |
"hello” |
在这个描述中,我们使用/test/
作为表ID(table ID)的占位符,
使用/floatVal/
和stringVal
作为列ID(column ID)的占位符。
(表中的每个列都具有表中唯一的ID。)
注意,在我们的编码中主键紧跟在表ID(table ID)之后。
这是CockroachDB SQL实现中索引描述(index-scans)的基础。
如果我们揭开现象看本质的话,我们将看到表的元数据如下:
|
|
---|---|
test Table ID |
1000 |
key Column ID |
1 |
floatVal Column ID |
2 |
stringVal Column ID |
3 |
在数字表单中,我们的表的键值对看起来像:
Key | Value |
---|---|
/1000/10/2 |
4.5 |
/1000/10/3 |
"hello” |
在这篇文章的剩余部分,我们将以这种符号的形式来描述键。
【你也许会想在每一个键前面都添加一个公共的前缀(/1000/10
)太浪费存储空间了,但是我们的底层存储引擎RocksDB,
通过将密钥的前缀压缩几乎消除了所有开销。】
精明的读者会注意到,在主键中存储列的键值对是不必要的,因为这些列的值已经在密钥本身中编码。
事实上,CockroachDB很喜欢这样。
注意,因为主键前缀的缘故,对于某一个特定行的所有列将彼此相邻存储(键和值存储在CockroachDB中的一个已排序的整体映射(Map)中,所以这个属性是免费的)。
这允许使用前缀扫描来检索特定行的值。这正是CockroachDB内部所做的。
查询:
SELECT * FROM test WHERE key = 10
将会被翻译成:
Scan(/test/10/, /test/10/Ω)
这个操作将只会检索行的两个键。Ω
表示最后一个可能的键的后缀。
然后,查询执行引擎将解码密钥以重构行。
空列值(Null Column Values)
这个故事有一个小小的转折:
除非特别指定了NOT NULL
,不然列的值时可能为NULL
的。
CockroachDB并不使用NULL
值,而是使用一对键值对的缺席来标记空值。
细心的同学可能已经看到了这里的一个问题了:按照这种分析,如果一行数据里面的所有非主键的列是NULL
的,那么我们将不会存储这组数据了。
为了解决这个问题,CocoroachDB始终为那些有主键但是没有列后缀的行数据添加一个前哨键(sentinal key)。
以行<10, 4.5, "hello”>
,他的前哨键将是/test/10
。啊哈!
二级索引(Secondary Indexes)
到目前为止,我们忽略了次要索引。让我们纠正这种疏忽:
CREATE INDEX foo ON test (stringVal)
这句SQL在列stringVal上创建了一个二级索引。
我们没有声明索引是唯一的,所以允许重复的值。
类似于表的行,我们将把所有索引数据存储在由表ID作为前缀的键中。
但是我们希望从行数据中分离索引数据。
我们通过引入索引ID来实现这一点,该索引对于表中的每个索引都是唯一的,包括主键索引(对不起,我们之前说谎了!):
/tableID/indexID/indexColumns[/columnID]
我们用上面的例子得到的键稍长一些:
Key | Value |
---|---|
/test/primary/10 |
Ø |
/test/primary/10/floatVal |
4.5 |
/test/primary/10/stringVal |
"hello” |
并且现在我们对于我们的索引foo也有一个单独的键:
Key | Value |
---|---|
/test/foo/”hello”/10 |
Ø |
您可能想知道为什么我们用主键值(/10)作为这个编码的后缀。
对于像foo样的非唯一索引,这是必要的,以便允许相同的值在多行中发生。
因为根据定义表的主键是唯一的,所以将其作为后缀添加到一个非唯一的键后面将会形成一个唯一的键。
一般而言,对于一个非唯一索引,CockroachDB将包含在主键中但是不包含在索引中的所有列追加到值后面。
现在让我们来看当我们把<4, NULL, "hello”>
插入到我们的表中会发生什么:
Key | Value |
---|---|
/test/primary/4 |
Ø |
/test/primary/4/stringVal |
"hello” |
/test/foo/"hello”/4 |
Ø |
所有的表中的数据合到一起看起来是这样的:
Key | Value |
---|---|
/test/primary/4 |
Ø |
/test/primary/4/stringVal |
"hello” |
/test/primary/10 |
Ø |
/test/primary/10/floatVal |
4.5 |
/test/primary/10/stringVal |
"hello” |
/test/foo/"hello”/4 |
Ø |
/test/foo/"hello”/10 |
Ø |
次级索引被用在 SELECT 中用于扫描一组小的集合的键。考虑:
SELECT key FROM test WHERE stringVal = "hello”
这个查询的规划师(query planner)会注意到在StringVal上有一个索引,并将此查询翻译成:
Scan(/test/foo/”hello”/, /test/foo/”hello"/Ω)
它将检索以下的键:
Key | Value |
---|---|
/test/foo/”hello”/4 |
Ø |
/test/foo/”hello”/10 |
Ø |
注意到这些键不仅包括索引列stringVal,而且还包括主键列所对应的键(译者注:4和10)。
CockroachDB将会注意到主键列上的键并且避免对整行数据进行不必要的查找。
最后,让我们看一下如何对唯一索引进行编码。
除了我们早先创建的索引foo,我们在创建一个uniqueFoo:
CREATE UNIQUE INDEX uniqueFoo ON test (stringVal)
与非唯一索引不同,唯一索引的键仅由索引的一部分组成。
键中存储的值是主键组成的列中去除掉索引中的列之后剩下的那些列。
我们的test表中的两行数据将被编码成:
Key | Value |
---|---|
/test/uniqueFoo/"hello” |
/4 |
/test/uniqueFoo/"hello” |
/10 |
我们使用ConditionalPut
函数,尝试写入数据之前检测这个键是否早已存在了,已检测是否违反了唯一性约束。
这就是CockroachDB如何简单地将SQL数据映射到键值存储中的方式。
请密切关注接下来关于查询分析(query analysis)、计划(planning)和执行(execution)的文章。
- 将SQL映射为键值存储的电子并不是在CockroachDB中独有的。这本质上也是MySQL on InnoDB、Sqlite4和其他很多数据库的设计。