diff --git a/README.md b/README.md index a884462719e0..269846ea1e34 100755 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ - [Prepared SQL 语句语法](sql/prepare.md) - [实用工具语句](sql/util.md) - [TiDB SQL 语法图](https://pingcap.github.io/sqlgram/) - - [JSON 支持](sql/json-functions-generated-column.md) + - [Generated Column](sql/generated-columns.md) - [Connectors 和 API](sql/connection-and-APIs.md) - [TiDB 事务隔离级别](sql/transaction-isolation.md) - [错误码与故障诊断](sql/error.md) diff --git a/sql/generated-columns.md b/sql/generated-columns.md new file mode 100644 index 000000000000..2d27746880fd --- /dev/null +++ b/sql/generated-columns.md @@ -0,0 +1,69 @@ +--- +title: Generated Column +summary: 本文档介绍如何使用 generated column +category: user guide +--- + +# Generated Column + +为了在功能上兼容 MySQL 5.7,TiDB 支持 generated column。Generated column 的主要的作用之一:从 JSON 数据类型中解出数据,并为该数据建立索引。 + +## 使用 generated column 对 JSON 建索引 + +MySQL 5.7 及 TiDB 都不能直接为 JSON 类型的列添加索引,即**不支持**如下表结构: + +```sql +CREATE TABLE person ( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + address_info JSON, + KEY (address_info) +); +``` + +为 JSON 列添加索引之前,首先必须抽取该列为 generated column。以 `city` generated column 为例,你可以添加索引: + +```sql +CREATE TABLE person ( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + address_info JSON, + city VARCHAR(64) AS (JSON_UNQUOTE(JSON_EXTRACT(address_info, '$.city'))) VIRTUAL, + KEY (city) +); +``` + +该表中,`city` 列是一个 **generated column**。顾名思义,此列由该表的其他列生成,对此列进行插入或更新操作时,并不能对之赋值。此列按需生成,并不存储在数据库中,也不占用内存空间,因而是**虚拟的**。`city` 列的索引**存储在数据库中**,并使用和 `varchar(64)` 类的其他索引相同的结构。 + +可使用 generated column 的索引,以提高如下语句的执行速度: + +```sql +SELECT name, id FROM person WHERE city = 'Beijing'; +``` + +如果 `$.city` 路径中无数据,则 `JSON_EXTRACT` 返回 `NULL`。如果你想增加约束:`city` 列必须是`NOT NULL`,则可按照以下方式定义 virtual column: + +```sql +CREATE TABLE person ( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + address_info JSON, + city VARCHAR(64) AS (JSON_UNQUOTE(JSON_EXTRACT(address_info, '$.city'))) VIRTUAL NOT NULL, + KEY (city) +); +``` + +`INSERT` 和 `UPDATE` 语句都会检查 virtual column 的定义。未通过有效性检测的行会返回错误: + +```sql +mysql> INSERT INTO person (name, address_info) VALUES ('Morgan', JSON_OBJECT('Country', 'Canada')); +ERROR 1048 (23000): Column 'city' cannot be null +``` + +## 局限性 + +目前 JSON and generated column 有以下局限性: + +- 不能通过 `ALTER TABLE` 增加 `STORED` 存储方式的 generated column; +- 不能通过 `ALTER TABLE` 在 generated column 上增加索引; +- 并未支持所有的 [JSON 函数](../sql/json-functions.md)。 diff --git a/sql/json-functions-generated-column.md b/sql/json-functions-generated-column.md deleted file mode 100644 index 9fc2d0ca79d3..000000000000 --- a/sql/json-functions-generated-column.md +++ /dev/null @@ -1,121 +0,0 @@ ---- -title: JSON 函数及 Generated Column -category: user guide ---- - -# JSON 函数及 Generated Column - -## 概述 - -为了在功能上兼容 MySQL 5.7 及以上,同时更好地支持文档类型存储,我们在最新版本的 TiDB 中加入了 JSON 的支持。TiDB 所支持的文档是指以 JSON 为编码类型的键值对的组合。用户可以在 TiDB 的表中使用 JSON 类型的字段,同时以生成列(generated column)的方式为 JSON 文档内部的字段建立索引。基于此,用户可以很灵活地处理那些 schema 不确定的业务,同时不必受限于传统文档数据库糟糕的读性能及匮乏的事务支持。 - -## JSON 功能介绍 - -TiDB 的 JSON 主要参考了 MySQL 5.7 的用户接口。例如,可以创建一个表,包含一个 JSON 字段来存储那些复杂的信息: - -```sql -CREATE TABLE person ( - id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(255) NOT NULL, - address_info JSON -); -``` - -当我们向表中插入数据时,便可以这样处理那些模式不确定的数据了: - -```sql -INSERT INTO person (name, address_info) VALUES ("John", '{"city": "Beijing"}'); -``` - -就这么简单!直接在 JSON 字段对应的位置上,放一个合法的 JSON 字符串,就可以向表中插入 JSON 了。TiDB 会解析这个文本,然后以一种更加紧凑、易于访问的二进制形式来保存。 - -当然,你也可以将其他类型的数据用 CAST 转换为 JSON: - -```sql -INSERT INTO person (name, address_info) VALUES ("John", CAST('{"city": "Beijing"}' AS JSON)); -INSERT INTO person (name, address_info) VALUES ("John", CAST('123' AS JSON)); -INSERT INTO person (name, address_info) VALUES ("John", CAST(123 AS JSON)); -``` - -现在,如果我们想查询表中所有居住在北京的用户,该怎么做呢?需要把数据全拉回来,然后在业务层进行过滤吗?不需要,和 MongoDB 等文档数据库相同,我们有在服务端支持用户各种复杂组合查询条件的能力。你可以这样写 SQL: - -```sql -SELECT id, name FROM person WHERE JSON_EXTRACT(address_info, '$.city') = 'Beijing'; -``` - -TiDB 支持 `JSON_EXTRACT` 函数,该函数与 MySQL 5.7 中 `JSON_EXTRACT` 的用法完全相同。这个函数的意思就是,从 `address_info` 这个文档中取出名为 `city` 这个字段。它的第二个参数是一个“路径表达式”,我们由此可以指定到底要取出哪个字段。关于路径表达式的完整语法描述比较复杂,我们还是通过几个简单的例子来了解其用法: - -```sql -SET @person = '{"name":"John","friends":[{"name":"Forest","age":16},{"name":"Zhang San","gender":"male"}]}'; - -SELECT JSON_EXTRACT(@person, '$.name'); -- gets "John" -SELECT JSON_EXTRACT(@person, '$.friends[0].age'); -- gets 16 -SELECT JSON_EXTRACT(@person, '$.friends[1].gender'); -- gets "male" -SELECT JSON_EXTRACT(@person, '$.friends[2].name'); -- gets NULL -``` - -除了插入、查询外,对 JSON 的修改也是支持的。总的来说,目前我们支持的 MySQL 5.7 的 JSON 函数如下表所示: - -* [JSON_EXTRACT](https://dev.mysql.com/doc/refman/5.7/en/json-search-functions.html#function_json-extract) -* [JSON_ARRAY](https://dev.mysql.com/doc/refman/5.7/en/json-creation-functions.html#function_json-array) -* [JSON_OBJECT](https://dev.mysql.com/doc/refman/5.7/en/json-creation-functions.html#function_json-object) -* [JSON_SET](https://dev.mysql.com/doc/refman/5.7/en/json-modification-functions.html#function_json-set) -* [JSON_REPLACE](https://dev.mysql.com/doc/refman/5.7/en/json-modification-functions.html#function_json-replace) -* [JSON_INSERT](https://dev.mysql.com/doc/refman/5.7/en/json-modification-functions.html#function_json-insert) -* [JSON_REMOVE](https://dev.mysql.com/doc/refman/5.7/en/json-modification-functions.html#function_json-remove) -* [JSON_TYPE](https://dev.mysql.com/doc/refman/5.7/en/json-attribute-functions.html#function_json-type) -* [JSON_UNQUOTE](https://dev.mysql.com/doc/refman/5.7/en/json-modification-functions.html#function_json-unquote) -* [JSON_MERGE](https://dev.mysql.com/doc/refman/5.7/en/json-modification-functions.html#function_json-merge) -* [JSON_CONTAINS](https://dev.mysql.com/doc/refman/5.7/en/json-search-functions.html#function_json-contains) -* [JSON_CONTAINS_PATH](https://dev.mysql.com/doc/refman/5.7/en/json-search-functions.html#function_json-contains-path) -* [JSON_LENGTH](https://dev.mysql.com/doc/refman/5.7/en/json-attribute-functions.html#function_json-length) - -直接从名字上,我们便能得出这些函数的大致用途,而且它们的语义也与 MySQL 5.7 完全一致,因此,想要查询它们具体的用法,我们可以直接查阅 MySQL 5.7 的[相关文档](https://dev.mysql.com/doc/refman/5.7/en/json-functions.html)。MySQL 5.7 的用户可以无缝迁移至 TiDB。 - -熟悉 MySQL 5.7 的用户会发现,TiDB 尚未完全支持 MySQL 5.7 中所有的 JSON 函数。通过 [TiDB #7546](https://github.com/pingcap/tidb/issues/7546) 可查看 TiDB 中添加新函数的进度。 - -## 使用生成列对 JSON 建索引 - -在有了上述的知识铺垫后,您可能会发现我们在查询 JSON 中的一个字段时,走的是全表扫描。使用 TiDB 的 `EXPLAIN` 语句时,一个比 MySQL 完备得多的结果会告诉我们,的确是全表扫描。那么,我们能否对 JSON 字段进行索引呢? - -首先,这种索引是错误的: - -```sql -CREATE TABLE person ( - id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(255) NOT NULL, - address_info JSON, - KEY (address_info) -); -``` - -这并非是因为技术上无法支持,而是因为对 JSON 的直接比较,本身就是没有意义的 —— 尽管我们可以人为地约定一些比较规则,比如 ARRAY 比所有的 OBJECT 都大 —— 但是这并没有什么用处。因此,正如 MySQL 5.7 所做的那样,我们禁止了直接在 JSON 字段上创建索引,而是通过生成列的方式,支持了对 JSON 文档内的某一字段建立索引: - -```sql -CREATE TABLE person ( - id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(255) NOT NULL, - address_info JSON, - city VARCHAR(64) AS (JSON_UNQUOTE(JSON_EXTRACT(address_info, '$.city'))) VIRTUAL, - KEY (city) -); -``` - -这个表中,`city` 列就是一个 **生成列**。顾名思义,该列由表中其他的列生成,而不能显式地在插入或更新时为它赋一个值。对于生成列,用户还可以指定其为 ``VIRTUAL`` 来避免它被显式地保存在记录中,而是在需要地时候再由其他列来生成,这对于列比较宽且需要节约存储空间地情况尤为有用。有了这个生成列,我们就可以在它上面建立索引了,在用户看来与常规的列便没什么两样,是不是很简单呢?而查询的时候,我们可以: - -```sql -SELECT name, id FROM person WHERE city = 'Beijing'; -``` - -这样,便可以走索引了! - -另外,需要注意的是,如果 JSON 文档中指定路径下的字段不存在,那么 JSON_EXTRACT 的结果会是 NULL ,这时,带有索引的生成列的值也就为 NULL 了。因此,如果这是用户不希望看到的,那也可以在生成列上增加 NOT NULL 约束,这样,当插入新的纪录算出来的 city 字段为 NULL 时,便可以检查出来了。 - -## 目前的一些限制 - -目前 JSON 及生成列仍然有一些限制: - -* 不能 ALTER TABLE 增加 STORED 存储方式的生成列; -* 不能 ALTER TABLE 在生成列上增加索引; - -这些功能,包括其他一些 JSON 函数的实现尚在开发过程中。 diff --git a/sql/json-functions.md b/sql/json-functions.md index e2be9595252d..1c88fc80a16a 100644 --- a/sql/json-functions.md +++ b/sql/json-functions.md @@ -1,26 +1,68 @@ --- -title: JSON 相关的函数和语法糖 +title: JSON 函数及语法糖 category: user guide --- -# JSON 相关的函数和语法糖 +# JSON 函数及语法糖 -| 函数或语法糖 | 功能描述 | +TiDB 支持 MySQL 5.7 GA 版本发布的大多数 JSON 函数。MySQL 5.7 发布后,又增加了更多 JSON 函数,TiDB 并未支持所有这些函数(参见[未支持的函数](#unsupported-functions))。 + +## 创建 JSON 值的函数 + +| 函数及语法糖 | 功能描述 | +| ------------------------------------------------------------------ | ---------------------------------------------------------- | +| [JSON_ARRAY([val[, val] ...])][json_array] | 根据一系列元素创建一个 JSON 文档 | +| [JSON_OBJECT(key, val[, key, val] ...)][json_object] | 根据一系列 K/V 对创建一个 JSON 文档 | + +## 搜索 JSON 值的函数 + +| 函数及语法糖 | 功能描述 | | ------------------------------------------------------------------ | ---------------------------------------------------------- | +| [JSON_CONTAINS(target, candidate[, path])][json_contains] | 通过返回 1 或 0 来表示目标 JSON 文档中是否包含给定的 candidate JSON 文档 | +| [JSON_CONTAINS_PATH(json_doc, one_or_all, path[, path] ...)][json_contains_path] | 通过返回 0 或 1 来表示一个 JSON 文档在给定路径是否包含数据 | | [JSON_EXTRACT(json_doc, path[, path] ...)][json_extract] | 从 JSON 文档中解出某一路径对应的子文档 | -| [JSON_UNQUOTE(json_val)][json_unquote] | 去掉 JSON 文档外面的引号 | -| [JSON_TYPE(json_val)][json_type] | 检查某 JSON 文档内部内容的类型 | -| [JSON_SET(json_doc, path, val[, path, val] ...)][json_set] | 在 JSON 文档中为某一路径设置子文档 | -| [JSON_INSERT(json_doc, path, val[, path, val] ...)][json_insert] | 在 JSON 文档中在某一路径下插入子文档 | -| [JSON_REPLACE(json_doc, path, val[, path, val] ...)][json_replace] | 替换 JSON 文档中的某一路径下的子文档 | -| [JSON_REMOVE(json_doc, path[, path] ...)][json_remove] | 移除 JSON 文档中某一路径下的子文档 | -| [JSON_MERGE(json_doc, json_doc[, json_doc] ...)][json_merge] | 将多个 JSON 文档合并成一个文档,其类型为数组 | -| [JSON_OBJECT(key, val[, key, val] ...)][json_object] | 根据一系列 K/V 对创建一个 JSON 文档 | -| [JSON_ARRAY([val[, val] ...])][json_array] | 根据一系列元素创建一个 JSON 文档 | -| -> | JSON_EXTRACT(doc, path_literal) 的语法糖 | -| ->> | JSON_UNQUOTE(JSONJSON_EXTRACT(doc, path_literal)) 的语法糖 | +| [->][json_short_extract] | 返回执行路径后面的 JSON 列的值;`JSON_EXTRACT(doc, path_literal)` 的语法糖 | +| [->>][json_short_extract_unquote] | 返回执行路径后面的 JSON 列的值和转义后的结果; `JSON_UNQUOTE(JSON_EXTRACT(doc, path_literal))` 的语法糖 | +| [JSON_KEYS(json_doc[, path])][json_keys] | 返回从 JSON 对象的顶级值作为 JSON array 的键,如果给定了路径参数,则从选定路径中获取顶级键 | + +## 修改 JSON 值的函数 + +| 函数及语法糖 | 功能描述 | +| --------------------------------- | ----------- | +| [JSON_INSERT(json_doc, path, val[, path, val] ...)][json_insert] | 在 JSON 文档中在某一路径下插入子文档 | +| [JSON_MERGE(json_doc, json_doc[, json_doc] ...)][json_merge] | 将多个 JSON 文档合并成一个文档,其类型为数组 | +| [JSON_REMOVE(json_doc, path[, path] ...)][json_remove] | 移除 JSON 文档中某一路径下的子文档 | +| [JSON_REPLACE(json_doc, path, val[, path, val] ...)][json_replace] | 替换 JSON 文档中的某一路径下的子文档 | +| [JSON_SET(json_doc, path, val[, path, val] ...)][json_set] | 在 JSON 文档中为某一路径设置子文档 | +| [JSON_UNQUOTE(json_val)][json_unquote] | 去掉 JSON 文档外面的引号 | + +## 返回 JSON 值属性的函数 + +| 函数及语法糖 | 功能描述 | +| --------------------------------- | ----------- | +| [JSON_LENGTH(json_doc[, path])][json_length] | 返回 JSON 文档的长度;如果路径参数已定,则返回该路径下值的长度 | +| [JSON_TYPE(json_val)][json_type] | 检查某 JSON 文档内部内容的类型 | + +## 未支持的函数 + +TiDB 暂未支持以下 JSON 函数。相关进展参见 [TiDB #7546](https://github.com/pingcap/tidb/issues/7546): + +* `JSON_APPEND` 及其别名 `JSON_ARRAY_APPEND` +* `JSON_ARRAY_INSERT` +* `JSON_DEPTH` +* `JSON_MERGE_PATCH` +* `JSON_MERGE_PRESERVE`,使用别名 `JSON_MERGE` 替代 +* `JSON_PRETTY` +* `JSON_QUOTE` +* `JSON_SEARCH` +* `JSON_STORAGE_SIZE` +* `JSON_VALID` +* `JSON_ARRAYAGG` +* `JSON_OBJECTAGG` [json_extract]: https://dev.mysql.com/doc/refman/5.7/en/json-search-functions.html#function_json-extract +[json_short_extract]: https://dev.mysql.com/doc/refman/5.7/en/json-search-functions.html#operator_json-column-path +[json_short_extract_unquote]: https://dev.mysql.com/doc/refman/5.7/en/json-search-functions.html#operator_json-inline-path [json_unquote]: https://dev.mysql.com/doc/refman/5.7/en/json-modification-functions.html#function_json-unquote [json_type]: https://dev.mysql.com/doc/refman/5.7/en/json-attribute-functions.html#function_json-type [json_set]: https://dev.mysql.com/doc/refman/5.7/en/json-modification-functions.html#function_json-set @@ -30,3 +72,10 @@ category: user guide [json_merge]: https://dev.mysql.com/doc/refman/5.7/en/json-modification-functions.html#function_json-merge [json_object]: https://dev.mysql.com/doc/refman/5.7/en/json-creation-functions.html#function_json-object [json_array]: https://dev.mysql.com/doc/refman/5.7/en/json-creation-functions.html#function_json-array +[json_keys]: https://dev.mysql.com/doc/refman/5.7/en/json-search-functions.html#function_json-keys +[json_length]: https://dev.mysql.com/doc/refman/5.7/en/json-attribute-functions.html#function_json-length +[json_valid]: https://dev.mysql.com/doc/refman/5.7/en/json-attribute-functions.html#function_json-valid +[json_quote]: https://dev.mysql.com/doc/refman/5.7/en/json-creation-functions.html#function_json-quote +[json_contains]: https://dev.mysql.com/doc/refman/5.7/en/json-search-functions.html#function_json-contains +[json_contains_path]: https://dev.mysql.com/doc/refman/5.7/en/json-search-functions.html#function_json-contains-path +[json_arrayagg]: https://dev.mysql.com/doc/refman/5.7/en/group-by-functions.html#function_json-arrayagg