CockroachDB权威指南——CockroachDB SQL

虽然CockroachDB有一些命令行工具,但所有应用程序与数据库之间的交互都通过SQL语言命令来媒介。

SQL是一种功能丰富的语言,拥有悠久的历史------我们在第一章中提到了一部分历史。对所有SQL语言特性的完整定义需要一本书来阐述,并且几乎会随着每次发布而迅速过时,因为SQL语言在不断发展。

因此,本章的目的是为你提供一个关于CockroachDB中使用的SQL语言的广泛概述,而不试图成为完整的参考资料。我们将采取任务导向的方法,涵盖最常见的SQL语言任务,特别是与CockroachDB SQL实现的独特特性相关的内容。

正如我们在第一章中描述的那样,SQL是一种声明式语言。SQL语句表示对查询和数据操作的逻辑请求,而不指定数据库应该如何实现这些请求。

CockroachDB SQL语言的完整参考资料可以在CockroachDB文档集中找到。对于SQL语言的更广泛回顾,可以参考O'Reilly的书《SQL in a Nutshell》。

本章中的一些示例使用了MovR示例数据集来说明各种SQL语言特性。我们在第三章中展示了如何安装示例数据。

SQL语言兼容性

CockroachDB与PostgreSQL实现的SQL:2016标准广泛兼容。SQL:2016标准包含了许多独立的模块,且没有一个主流数据库实现了所有标准。然而,PostgreSQL对SQL的实现可以说是数据库社区中最接近"标准"的实现。

CockroachDB在一些方面与PostgreSQL有所不同(例如,CockroachDB不支持PostgreSQL的XML函数)。有关这些差异的详细信息,请参见PostgreSQL兼容性部分。

使用SELECT查询数据

虽然我们需要在查询数据之前创建并填充表,但从SELECT语句开始是合理的,因为SELECT语句的许多特性出现在其他类型的SQL中------例如,UPDATE中的子查询------对于数据科学家和分析师来说,SELECT语句通常是他们需要学习的唯一SQL语句。

SELECT语句(如图4-1所示)是关系型查询的主力军,具有复杂且丰富的语法。CockroachDB的SELECT语句实现了标准SELECT的典型特性,仅有少数CockroachDB特有的功能。

在以下各节中,我们将讨论SELECT语句的主要元素以及可以在其中包含的函数和运算符。

SELECT 列表

一个简单的SQL语句由一个SELECT语句和标量表达式(即返回单一值的表达式)组成。例如:

vbnet 复制代码
SELECT CONCAT('Hello from CockroachDB at ',
              CAST (NOW() as STRING)) as hello;

SELECT列表包括一个由逗号分隔的表达式列表,这些表达式可以包含常量、函数和运算符的组合。CockroachDB SQL语言支持所有常见的SQL运算符。运算符和函数的完整列表可以在CockroachDB文档中找到。

FROM 子句

FROM子句是将表数据附加到SELECT语句的主要方式。在最简单的情况下,可以通过完全表扫描来获取表中的所有行和列:

sql 复制代码
SELECT * FROM rides;

表名可以使用AS子句或直接在表名后跟别名进行别名化。然后可以在查询中任何地方使用该别名来引用表。列名也可以别名化。例如,以下语句是等效的:

vbnet 复制代码
SELECT name FROM users;
SELECT u.name FROM users u;
SELECT users.name FROM users;
SELECT users.name AS user_name FROM users;
SELECT u.name FROM users AS u;

连接(Joins)

连接允许基于某些公共列值将两个或更多表的结果合并。

INNER JOIN是默认的JOIN操作。在此连接中,一个表中的行与另一个表中的行基于某些公共("关键")值进行连接。没有匹配行的行不会包含在结果中。例如,以下查询在movr数据库中连接车辆和乘车信息:

sql 复制代码
SELECT v.id, v.ext, r.start_time, r.start_address
  FROM vehicles v
  INNER JOIN rides r
    ON (r.vehicle_id = v.id);

注意,未参与任何乘车的车辆将不会包含在结果集中。

ON子句指定连接两个表的条件------在前面的查询中,rider表中的vehicle_id列与vehicles表中的id列进行了匹配。如果JOIN的列在两个表中名称相同,则可以使用USING子句作为便捷的简写。这里我们使用公共名称列连接users和user_ride_counts:

sql 复制代码
SELECT *
  FROM users u
  JOIN user_ride_counts urc
 USING (name);

OUTER JOIN允许即使没有匹配的行也将其包含在内。未在OUTER JOIN表中找到的行将由NULL值表示。LEFT和RIGHT确定哪张表可能有缺失值。例如,以下查询打印出users表中的所有用户,即使某些用户没有与促销代码相关联:

sql 复制代码
SELECT u.name, upc.code
  FROM users u
  LEFT OUTER JOIN user_promo_codes upc
    ON (u.id = upc.user_id);

RIGHT OUTER JOIN会反转默认的(LEFT)OUTER JOIN。因此,这个查询与前一个查询相同,因为现在users表是连接中的"右"表:

sql 复制代码
SELECT DISTINCT u.name, upc.code
  FROM user_promo_codes upc
  RIGHT OUTER JOIN users u
    ON (u.id = upc.user_id);

反连接(Anti-Joins)

通常需要选择一个表中没有在另一个结果集中匹配的所有行。这被称为反连接,虽然SQL没有这种概念的语法,但通常通过使用子查询和IN或EXISTS子句来实现。以下示例使用EXISTS和IN运算符说明了一个反连接。

每个示例选择没有同时也是员工的用户:

sql 复制代码
SELECT *
  FROM users
 WHERE id NOT IN
       (SELECT id FROM employees);

这个查询返回相同的结果,但使用了相关子查询(我们将在后续部分详细讨论子查询):

sql 复制代码
SELECT *
   FROM users u
  WHERE NOT EXISTS
        (SELECT id
           FROM employees e
          WHERE e.id = u.id);

交叉连接(Cross Joins)

CROSS JOIN表示左表中的每一行都应该与右表中的每一行进行连接。通常,除非其中一个表只有一行或是横向相关的子查询,否则这是一种灾难性的操作(参见"相关子查询")。

集合操作(Set Operations)

SQL实现了一些直接处理结果集的操作。这些操作统称为"集合操作",允许对结果集进行连接、减法或叠加。

最常见的集合操作是UNION运算符,它返回两个结果集的和。默认情况下,会消除每个结果集中的重复项。与此相对,UNION ALL操作将返回两个结果集的和,包括任何重复项。以下示例返回客户和员工的列表。既是客户又是员工的人只会列出一次:

sql 复制代码
SELECT name, address
  FROM customers
 UNION
SELECT name, address
  FROM employees;

INTERSECT返回两个结果集中都存在的行。此查询返回既是客户又是员工的人:

sql 复制代码
SELECT name, address
  FROM customers
 INTERSECT
SELECT name, address
  FROM employees;

EXCEPT返回第一个结果集中在第二个结果集中不存在的行。此查询返回不是员工的客户:

sql 复制代码
SELECT name, address
  FROM customers
 EXCEPT
SELECT name, address
  FROM employees;

所有集合操作要求组成查询返回相同数量的列,并且这些列的数据类型兼容。

分组操作(Group Operations)

聚合操作允许生成汇总信息,通常是在对行进行分组后。可以使用GROUP BY运算符对行进行分组。如果进行了分组,则SELECT列表必须仅包含GROUP BY子句中包含的列和聚合函数。

最常见的聚合函数如下表所示:

表4-1. 聚合函数

函数 描述
AVG 计算该组的平均值
COUNT 返回该组中的行数
MAX 返回该组中的最大值
MIN 返回该组中的最小值
STDDEV 返回该组的标准差
SUM 返回该组中所有值的总和

以下示例生成每个城市的汇总乘车信息:

sql 复制代码
SELECT u.city, SUM(urc.rides), AVG(urc.rides), MAX(urc.rides)
  FROM users u
  JOIN user_ride_counts urc
 USING (name)
 GROUP BY u.city;

子查询

子查询是出现在另一个SQL语句中的SELECT语句。这样的"嵌套"SELECT语句可以在多种SQL上下文中使用,包括SELECT、DELETE、UPDATE和INSERT语句。

以下语句使用子查询来计算共享最大乘车时长的乘车数量:

sql 复制代码
SELECT COUNT(*) FROM rides
 WHERE (end_time - start_time) =
 	(SELECT MAX(end_time - start_time) FROM rides );

子查询也可以在FROM子句中使用,任何可能出现表或视图定义的地方都可以使用子查询。这条查询生成一个结果,将每个乘车与该城市的平均乘车时长进行比较:

sql 复制代码
SELECT id, city, (end_time - start_time) ride_duration, avg_ride_duration
  FROM rides
  JOIN (SELECT city,
               AVG(end_time - start_time) avg_ride_duration
	   FROM rides
	  GROUP BY city)
 USING(city);

相关子查询

相关子查询是指子查询引用父查询或操作中的值。子查询为父结果集中的每一行返回可能不同的结果。我们在本章前面做"反连接"时看到过一个相关子查询的例子。

sql 复制代码
SELECT *
   FROM users u
  WHERE NOT EXISTS
        (SELECT id
           FROM employees e
          WHERE e.id = u.id);

子查询通常可以用于执行与连接等效的操作。在许多情况下,查询优化器会将这些语句转换为连接,以简化优化过程。

横向子查询(Lateral Subquery)

当子查询用于连接时,LATERAL 关键字表示该子查询可以访问在前面的FROM表表达式中生成的列。例如,在以下查询中,LATERAL 关键字允许子查询访问来自users表的列:

sql 复制代码
SELECT name, address, start_time
   FROM users CROSS JOIN
        LATERAL (SELECT *
                   FROM rides
                  WHERE rides.start_address = users.address ) r;

这个例子稍显牵强,显然我们可以构建一个简单的JOIN来更自然地执行这个查询。LATERAL连接的真正优势在于允许子查询访问FROM子句中其他子查询中的计算列。Andy Woods的CockroachDB博客文章描述了一个更复杂的横向子查询的例子。

WHERE 子句

WHERE子句在SELECT、UPDATE和DELETE语句中是常见的。它指定了一组逻辑条件,所有行必须满足这些条件才能被SQL语句返回或处理。

公共表表达式(Common Table Expressions)

带有大量子查询的SQL语句可能难以阅读和维护,尤其是当同一子查询需要在查询的多个上下文中使用时。为此,SQL通过WITH子句支持公共表表达式。图4-2显示了公共表表达式的语法。

公共表表达式的最简单形式只是一个命名的查询块,可以在任何可以使用表表达式的地方应用。例如,在这里我们使用WITH子句创建一个公共表表达式 riderRevenue,然后在主查询的FROM子句中引用它:

vbnet 复制代码
WITH riderRevenue AS (
	  SELECT u.id, SUM(r.revenue) AS sumRevenue
	    FROM rides r JOIN "users" u
	    ON (r.rider_id = u.id)
	   GROUP BY u.id)
SELECT * FROM "users" u2
         JOIN riderRevenue rr USING (id)
 ORDER BY sumRevenue DESC;

递归公共表表达式(RECURSIVE)

RECURSIVE 子句允许公共表表达式引用其自身,这使得查询可能返回任意高(甚至是无限)的结果集。例如,如果employees表包含一个manager_id列,该列引用同一表中经理的行,那么我们可以如下打印出员工和经理的层级关系:

sql 复制代码
WITH RECURSIVE employeeMgr AS (
  SELECT id, manager_id, name, NULL AS manager_name, 1 AS level
    FROM employees managers
   WHERE manager_id IS NULL
  UNION ALL
  SELECT subordinates.id, subordinates.manager_id,
         subordinates.name, managers.name, managers.level + 1
    FROM employeeMgr managers
    JOIN employees subordinates
      ON (subordinates.manager_id = managers.id)
)
SELECT * FROM employeeMgr;

物化公共表表达式(MATERIALIZED)

MATERIALIZED 子句强制CockroachDB将公共表表达式的结果存储为临时表,而不是在每次出现时重新执行它。如果公共表表达式在查询中被多次引用,这种方法可能会非常有用。

ORDER BY

ORDER BY 子句允许查询结果按排序顺序返回。图4-3显示了ORDER BY的语法。注意,图4-4中对 sortby_index 进行了扩展。

在最简单的形式中,ORDER BY 可以接受一个或多个来自SELECT列表的列表达式或列号。

在以下示例中,我们按列号进行排序:

sql 复制代码
SELECT city, start_time, (end_time - start_time) duration
   FROM rides r
  ORDER BY 1, 3 DESC;

在这个例子中,我们按列表达式进行排序:

sql 复制代码
SELECT city, start_time, (end_time - start_time) duration
   FROM rides r
  ORDER BY city, (end_time - start_time) DESC;

如图4-4所示,你还可以按索引进行排序。

在以下示例中,行将根据citystart_time进行排序,因为这些列在索引中被指定:

scss 复制代码
CREATE INDEX rides_start_time ON rides (city, start_time);

SELECT city, start_time, (end_time - start_time) duration
  FROM rides
 ORDER BY INDEX rides@rides_start_time;

使用ORDER BY INDEX可以确保索引会被直接用于按排序顺序返回行,而不是在检索行后执行排序操作。有关优化包含ORDER BY语句的更多建议,请参见第8章。

窗口函数(Window Functions)

窗口函数是对结果集合的一个子集(即"窗口")进行操作的函数。图4-5显示了窗口函数的语法。

PARTITION BYORDER BY 创建了一种"虚拟表",窗口函数在其中进行操作。例如,这个查询列出了按收入排序的前10名乘车记录,并显示了总收入和城市收入的百分比:

sql 复制代码
SELECT city, r.start_time, revenue,
       revenue * 100 / SUM(revenue) OVER () AS pct_total_revenue,
       revenue * 100 / SUM(revenue) OVER (PARTITION BY city) AS pct_city_revenue
  FROM rides r
 ORDER BY 5 DESC
 LIMIT 10;

有一些聚合函数是窗口函数特有的。RANK() 为相关窗口中的现有行排名,DENSE_RANK() 做同样的事情,但允许没有"缺失"的排名。LEADLAG 提供对相邻分区中函数的访问。

例如,这个查询返回前10名乘车记录,并显示每个乘车记录的总排名和城市排名:

sql 复制代码
SELECT city, r.start_time, revenue,
       RANK() OVER (ORDER BY revenue DESC) AS total_revenue_rank,
       RANK() OVER (PARTITION BY city ORDER BY revenue DESC) AS city_revenue_rank
  FROM rides r
 ORDER BY revenue DESC
 LIMIT 10;

其他SELECT子句

LIMIT 子句限制了SELECT返回的行数,而 OFFSET 子句则"跳过"一定数量的行。这对于分页显示结果集非常有用,但通常使用过滤条件来导航到下一个结果集会更高效------否则,每个请求都需要重新读取并丢弃越来越多的行。

CockroachDB 数组

ARRAY 类型允许定义一列为一维元素数组,每个元素共享相同的数据类型。我们将在下一章中讨论数据建模时如何使用数组。严格来说,虽然数组有用,但它们违反了关系模型,应该谨慎使用。

ARRAY 变量是通过在列的数据类型后添加 []ARRAY 来定义的。例如:

sql 复制代码
CREATE TABLE arrayTable (arrayColumn STRING[]);
CREATE TABLE anotherTable (integerArray INT ARRAY);

ARRAY 函数允许我们向数组中插入多个项:

sql 复制代码
INSERT INTO arrayTable VALUES (ARRAY['sky', 'road', 'car']);
SELECT * FROM arrayTable;
   arrayColumn
------------------
  {sky,road,car}

我们可以使用熟悉的数组元素符号访问数组的单个元素:

markdown 复制代码
SELECT arrayColumn[2] FROM arrayTable;
  arrayColumn
---------------
  road

@> 运算符可用于查找包含一个或多个元素的数组:

sql 复制代码
SELECT * FROM arrayTable WHERE arrayColumn @> ARRAY['road'];
   arrayColumn
------------------
  {sky,road,car}

我们可以使用 array_append 函数向现有数组添加元素,使用 array_remove 函数删除元素:

sql 复制代码
UPDATE arrayTable
   SET arrayColumn = array_append(arrayColumn, 'cat')
  WHERE arrayColumn @> ARRAY['car']
  RETURNING arrayColumn;
     arrayColumn
----------------------
  {sky,road,car,cat}

UPDATE arrayTable
   SET arrayColumn = array_remove(arrayColumn, 'car')
  WHERE arrayColumn @> ARRAY['car']
  RETURNING arrayColumn;
   arrayColumn
------------------
  {sky,road,cat}

最后,unnest 函数将数组转换为表格形式的结果------每个数组元素对应一行。这可以用来将数组内容与数据库中以关系形式存储的数据"连接"起来。我们将在下一章中展示一个例子:

sql 复制代码
SELECT unnest(arrayColumn)
  FROM arrayTable;
  unnest
----------
  sky
  road
  cat

与JSON一起工作

JSONB 数据类型允许我们将JSON文档存储到列中,CockroachDB提供了运算符和函数来帮助我们处理JSON。

以下示例中,我们创建了一个表,主键为customerid,所有数据存储在一个JSONB列jsondata中。我们可以使用 jsonb_pretty 函数以格式化的方式检索JSON:

sql 复制代码
SELECT jsonb_pretty(jsondata)
   FROM customersjson WHERE customerid = 1;
                     jsonb_pretty
------------------------------------------------------
  {
      "Address": "1913 Hanoi Way",
      "City": "Sasebo",
      "Country": "Japan",
      "District": "Nagasaki",
      "FirstName": "MARY",
      "LastName": "Smith",
      "Phone": 886780309,
      "_id": "5a0518aa5a4e1c8bf9a53761",
      "dateOfBirth": "1982-02-20T13:00:00.000Z",
      "dob": "1982-02-20T13:00:00.000Z",
      "randValue": 0.47025846594884335,
      "views": [
          {
              "filmId": 611,
              "title": "MUSKETEERS WAIT",
              "viewDate": "2013-03-02T05:26:17.645Z"
          },
          {
              "filmId": 308,
              "title": "FERRIS MOTHER",
              "viewDate": "2015-07-05T20:06:58.891Z"
          },
          {
              "filmId": 159,
              "title": "CLOSER BANG",
              "viewDate": "2012-08-04T19:31:51.698Z"
          },
          /* 一些数据已被删除 */
      ]
  }

每个JSON文档包含一些顶层属性和一个嵌套的文档数组,记录了他们观看的电影的详细信息。

我们可以使用 -> 运算符在SELECT子句中引用JSON特定属性:

sql 复制代码
SELECT jsondata->'City' AS City
  FROM customersjson WHERE customerid = 1;
    city
------------
  "Sasebo"

->> 运算符与之类似,但返回的是格式化为文本的数据,而不是JSON。

如果我们想在JSONB列中搜索数据,可以使用 @> 运算符:

sql 复制代码
SELECT COUNT(*) FROM customersjson
 WHERE jsondata @> '{"City": "London"}';
  count
---------
      3

我们可以使用 ->> 运算符来获得相同的结果:

sql 复制代码
SELECT COUNT(*) FROM customersjson
 WHERE jsondata->>'City' = 'London';
  count
---------
      3

->>@> 运算符可能具有不同的性能特征。特别是,->> 可能会利用倒排索引,而 @> 则使用表扫描。

我们可以使用 jsonb_eachjsonb_object_keys 函数检查JSON文档的结构。jsonb_each 每个属性返回一行,而 jsonb_object_keys 只返回属性键。如果你不知道JSONB列中存储了什么,这些函数非常有用。

jsonb_array_elements 为JSON数组中的每个元素返回一行。例如,以下查询展开了特定客户的views数组,并计算他们观看的电影数量:

sql 复制代码
SELECT COUNT(jsonb_array_elements(jsondata->'views'))
  FROM customersjson
 WHERE customerid = 1;
  count
---------
     37
(1 row)

SELECT语句总结

SELECT语句可能是数据库编程中使用最广泛的语句,提供了广泛的功能。即使我们四个人在这个领域工作了几十年,仍然无法掌握SELECT功能的每一个细节。然而,在这里,我们尽力为你提供了该语言最重要的方面。要获取更深入的了解,请参阅CockroachDB文档集。

尽管一些数据库专业人士几乎只使用SELECT语句,但大多数人也会创建和操作数据。在接下来的各节中,我们将探讨支持这些活动的语言特性。

创建表和索引

在关系型数据库中,数据只能添加到预定义的表中。这些表是通过CREATE TABLE语句创建的。可以创建索引以强制执行唯一性约束或提供数据的快速访问路径。索引可以在CREATE TABLE语句中定义,也可以通过单独的CREATE INDEX语句来创建。

数据库模式的结构对数据库的性能以及数据库的可维护性和可用性形成了关键约束。我们将在第5章中讨论数据库设计的关键考虑因素。现在,让我们创建一些简单的表。

我们使用CREATE TABLE语句在数据库中创建表。图4-6提供了CREATE TABLE语句的简化语法。

下一个示例展示了一个简单的CREATE TABLE语句。它创建了一个名为mytable的表,并包含一个名为mycolumn的列。mycolumn列只能存储整数值:

sql 复制代码
CREATE TABLE mytable
(
      mycolumn int
);

CREATE TABLE语句必须定义表中包含的列,并且可以选择性地定义与表相关的索引、列族、约束和分区。例如,movr数据库中的rides表的CREATE TABLE语句可能如下所示:

sql 复制代码
CREATE TABLE public.rides (
	id UUID NOT NULL,
	city VARCHAR NOT NULL,
	vehicle_city VARCHAR NULL,
	rider_id UUID NULL,
	vehicle_id UUID NULL,
	start_address VARCHAR NULL,
	end_address VARCHAR NULL,
	start_time TIMESTAMP NULL,
	end_time TIMESTAMP NULL,
	revenue DECIMAL(10,2) NULL,
	CONSTRAINT "primary" PRIMARY KEY (city ASC, id ASC),
	CONSTRAINT fk_city_ref_users
	   FOREIGN KEY (city, rider_id)
	   REFERENCES public.users(city, id),
	CONSTRAINT fk_vehicle_city_ref_vehicles
	   FOREIGN KEY (vehicle_city, vehicle_id)
	   REFERENCES public.vehicles(city, id),
	INDEX rides_auto_index_fk_city_ref_users
	   (city ASC, rider_id ASC),
	INDEX rides_auto_index_fk_vehicle_city_ref_vehicles
          (vehicle_city ASC, vehicle_id ASC),
	CONSTRAINT check_vehicle_city_city
           CHECK (vehicle_city = city)
);

CREATE TABLE语句指定了额外的列、它们的可空性、主键和外键、索引以及表值上的约束。

图4-6中的相关子句在表4-2中列出。

表4-2. CREATE TABLE 选项

选项 描述
column_def 列的定义,包括列名、数据类型和可空性。特定于列的约束也可以在此包含,但最好单独列出所有约束。
index_def 要在表上创建的索引的定义。与CREATE INDEX相同,但没有前缀CREATE动词。
table_constraint 表上的约束,如主键、外键或检查约束。请参见"约束"部分获取约束语法。
family_def 将列分配到列族。有关列族的更多信息,请参见第2章。

现在,让我们逐一查看这些CREATE TABLE选项。

列定义

列定义由列名、数据类型、可空性状态、默认值以及可能的列级约束定义组成。至少必须指定列名和数据类型。图4-7显示了列定义的语法。

虽然约束可以直接在列定义中指定,但它们也可以单独列出在列定义下方。许多实践者倾向于将约束单独列出,因为这样可以将所有约束(包括多列约束)集中在一起。

计算列

CockroachDB 允许表包含计算列,而在其他一些数据库中,计算列通常需要通过视图定义来实现:

css 复制代码
column_name AS expression [STORED|VIRTUAL]

VIRTUAL计算列每次被引用时都会被评估。STORED表达式在创建时存储在数据库中,因此不需要每次都重新计算。

例如,以下表定义将 firstNamelastName 拼接为 fullName 列:

sql 复制代码
CREATE TABLE people
 (
     id INT PRIMARY KEY,
     firstName VARCHAR NOT NULL,
     lastName VARCHAR NOT NULL,
     dateOfBirth DATE NOT NULL,
     fullName STRING AS (CONCAT(firstName, ' ', lastName)) STORED,
     age INT AS (now() - dateOfBirth) STORED
 );

计算列不能依赖于上下文。即,计算的值不能随时间变化或是非确定性的。例如,以下示例中的计算列 age 不会按每次重新计算,因为它是静态的,而不是每次都更新。虽然停止衰老在现实生活中可能很美好,但我们通常希望 age 列随着时间推移而增加。

sql 复制代码
CREATE TABLE people
 (
     id INT PRIMARY KEY,
     firstName VARCHAR NOT NULL,
     lastName VARCHAR NOT NULL,
     dateOfBirth TIMESTAMP NOT NULL,
     fullName STRING AS (CONCAT(firstName, ' ', lastName)) STORED,
     age INT AS (now() - dateOfBirth) STORED
 );

数据类型

CockroachDB 的基本数据类型如下表所示:

表4-3. CockroachDB 数据类型

类型 描述 示例
ARRAY 一维、单索引、同质的数组,包含任何非数组的数据类型。 {"sky", "road", "car"}
BIT 一串二进制数字(位)。 B'10010101'
BOOL 布尔值。 true
BYTES 一串二进制字符。 b'\141\061\142\062\143\063'
COLLATE 允许根据语言和国家/地区的规则对字符串值进行排序。 'a1b2c3' COLLATE en
DATE 日期。 DATE '2016-01-25'
DECIMAL 精确的定点数。 1.2345
ENUM 新增于v20.2:由一组静态值组成的用户定义数据类型。 ENUM ('club', 'diamond', 'heart', 'spade')
FLOAT 64位不精确的浮点数。 3.141592653589793
INET IPv4 或 IPv6 地址。 192.168.0.1
INT 一个有符号整数,最多64位。 12345
INTERVAL 时间跨度。 INTERVAL '2h30m30s'
JSONB JSON 数据。 '{"first_name": "Lola", "last_name": "Dog", "location": "NYC", "online" : true, "friends" : 547}'
SERIAL 一种伪类型,用于生成唯一的递增数字。 148591304110702593
STRING 一串 Unicode 字符。 'a1b2c3'
TIME 存储 UTC 时间。 TIME '01:23:45.123456'
TIMETZ 转换时间值为指定时区偏移的时间。 TIMETZ '01:23:45.123456-5:00'
TIMESTAMP 存储 UTC 的日期和时间配对。 TIMESTAMP '2016-01-25 10:10:10'
TIMESTAMPTZ 转换为带时区偏移的时间戳。 TIMESTAMPTZ '2016-01-25 10:10:10-05:00'
TSQUERY 用于全文搜索的词汇列表。 'lazi' & 'dog'
TSVECTOR 含有可选位置数据的词汇列表。 'dog':2 'lazi':1
UUID 128位的十六进制值。 7f9c24e8-3b12-4fef-91e0-56a2d5a246ec
VECTOR 一个固定长度的浮点数数组,表示n维空间中的点。 [0.1259227, 0.0318873, 0.4660917]

注意,其他数据类型的名称可能会与这些CockroachDB基本类型关联。例如,PostgreSQL类型BIGINT和SMALLINT与CockroachDB类型INT关联。

在CockroachDB中,可以通过将数据类型追加到表达式后使用"::"进行数据类型转换。例如:

sql 复制代码
SELECT revenue::int FROM rides;

CAST 函数也可以用于转换数据类型,并且与其他数据库和SQL标准更广泛兼容。例如:

sql 复制代码
SELECT CAST(revenue AS int) FROM rides;

主键

如我们所知,主键唯一地定义了表中的一行。在CockroachDB中,主键是强制性的,因为所有表都根据主键的范围在集群中分布。如果未指定主键,系统将自动为你生成一个主键。

在其他数据库中,通常使用 AUTOINCREMENT 等子句来定义自动生成的主键。分布式数据库中主键的生成是一个重要问题,因为主键用于在集群的节点间分配数据。我们将在下一章讨论主键生成的选项,但目前我们只需注意,你可以使用UUID数据类型与gen_random_uuid()函数作为默认值来生成随机化的主键KV:

sql 复制代码
CREATE TABLE people (
        id UUID NOT NULL DEFAULT gen_random_uuid(),
        firstName VARCHAR NOT NULL,
        lastName VARCHAR NOT NULL,
        dateOfBirth DATE NOT NULL
 );

这种模式被认为是最佳实践,可以确保主键在集群中的均匀分布。其他自动生成主键的选项将在第5章讨论。

约束

CONSTRAINT 子句指定所有行必须满足的条件。在某些情况下,CONSTRAINT 关键字可以省略,例如在定义列约束或特定约束类型(如主键或外键)时。图4-8显示了约束定义的一般形式。

UNIQUE 约束

UNIQUE 约束要求列或列列表中的所有值都是唯一的。

PRIMARY KEY

PRIMARY KEY 实现了一组必须唯一的列,这些列也可以在另一张表中作为外键(FOREIGN KEY)的对象。PRIMARY KEYUNIQUE 约束都要求创建一个隐式索引。如果需要,可以在 USING 子句中指定索引的物理存储特性。USING INDEX 子句的选项与 CREATE INDEX 语句中的用法相同。

NOT NULL

NOT NULL 表示相关列不能为 NULL。此选项仅适用于列约束,但通过使用表级的 CHECK 约束也可以达到相同的效果。

CHECK

CHECK 定义了一个表达式,该表达式必须对表中的每一行评估为真。我们将在第5章讨论创建约束的最佳实践。

合理使用约束可以帮助确保数据质量,并为数据库提供一定程度的自文档化。然而,一些约束对性能有显著的影响;我们将在第5章讨论这些影响。

索引

索引可以通过 CREATE INDEX 语句创建,或者可以在 CREATE TABLE 语句中包含一个索引定义。

我们在第2章中已经详细讨论了索引,并将在架构设计和性能优化章节(分别是第5章和第8章)中继续讨论索引。有效的索引是成功实现性能优化的CockroachDB部署的关键因素之一。

图4-9展示了一个简单的CockroachDB CREATE INDEX 语句的语法。

我们在第2章中探讨了CockroachDB索引的内部实现。从性能角度来看,CockroachDB索引的行为与其他数据库中的索引非常相似------它们提供了一种快速的访问方式,用于定位具有特定非主键KV集合的行。例如,如果我们只想查找具有特定名字和出生日期的行,我们可以创建以下多列索引:

arduino 复制代码
CREATE INDEX people_namedob_ix ON people
 (lastName, firstName, dateOfBirth);

如果我们想进一步确保没有两行具有相同的名字和出生日期值,我们可以创建一个唯一索引:

sql 复制代码
CREATE UNIQUE INDEX people_namedob_ix ON people
 (lastName, firstName, dateOfBirth);

STORING 子句允许我们在索引中存储额外的数据,这可以让我们仅通过索引满足查询。例如,这个索引可以满足检索特定名字和出生日期的电话号码的查询:

scss 复制代码
CREATE UNIQUE INDEX people_namedob_ix ON people
 (lastName, firstName, dateOfBirth) STORING (phoneNumber);

倒排索引

倒排索引可以用于索引数组中的元素或JSON文档中的属性。我们在第2章中探讨了倒排索引的内部实现。倒排索引还可以用于空间数据。

例如,假设我们的people表使用JSON文档来存储一个人的属性:

vbnet 复制代码
CREATE TABLE people
 ( id UUID NOT NULL DEFAULT gen_random_uuid(),
   personData JSONB );

INSERT INTO people (personData)
VALUES('{
      "firstName":"Guy",
      "lastName":"Harrison",
      "dob":"21-Jun-1960",
      "phone":"0419533988",
      "photo":"eyJhbGciOiJIUzI1NiIsI..."
 }');

我们可以创建一个倒排索引,如下所示:

scss 复制代码
CREATE INVERTED INDEX people_inv_idx ON
people(personData);

这将支持查询JSON文档,如下所示:

sql 复制代码
SELECT *
FROM people
WHERE personData @> '{"phone":"0419533988"}';

请记住,倒排索引会对JSON文档中的每个属性进行索引,而不仅仅是你希望搜索的那些属性。这可能导致非常大的索引。因此,你可能会发现创建计算列来表示JSON属性并对该计算列进行索引更为有用:

sql 复制代码
ALTER TABLE people ADD phone STRING AS (personData->>'phone') VIRTUAL;

CREATE INDEX people_phone_idx ON people(phone);

哈希分片索引

如果你正在处理一个必须基于顺序键进行索引的表,应该使用哈希分片索引。哈希分片索引将顺序流量均匀地分布到多个范围中,从而消除单一范围热点,并在对顺序键索引进行写操作时提供更好的性能,虽然这会对读取性能带来一些小的成本:

sql 复制代码
CREATE TABLE people
( id INT PRIMARY KEY,
  firstName VARCHAR NOT NULL,
  lastName VARCHAR NOT NULL,
  dateOfBirth timestamp NOT NULL,
  phoneNumber VARCHAR NOT NULL,
  serialNo SERIAL,
  INDEX serialNo_idx (serialNo) USING HASH WITH BUCKET_COUNT=4);

我们将在下一节中详细讨论哈希分片索引以及其他更高级的索引主题。

向量(Vectors)

CockroachDB 的 VECTOR 数据类型与 PostgreSQL 的流行扩展 pgvector 兼容,允许你将 CockroachDB 嵌入到你的大型语言模型(LLM)或机器学习(ML)基础设施中。

向量通过在 n 维空间中表示特征,使得语义相似度搜索变得高效,其中,距离的接近性表示概念上的相关性。让我们通过一些具体的例子来更好地理解这一点。

首先,我们将创建一个包含 VECTOR 列的表。假设我们是一个在线零售商,并且希望根据客户购买情况提供产品推荐:

sql 复制代码
CREATE TABLE customer (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email STRING NOT NULL UNIQUE,
  vector VECTOR(7)
);

向量的维度是通过创建时使用的数字来定义的。在表4-4中,我们将此向量配置为存储七个维度。乍一看,这似乎是任意的,但其实背后有原因。

为了以数字格式表示客户,以便进行距离搜索,我们需要存储关于客户的信息,这些信息将有助于在向量中进行距离搜索。

表4-4. 向量的七个特征

属性 示例 编码类型
年龄 0.3780123 最小/最大(值在0到1之间,表示客户的年龄位于某个最小和最大年龄范围之间)
性别 0, 0, 0, 1 独热编码(性别之间没有序数关系,因此使用独热编码将性别编码为唯一的浮动数组)
位置 0.0188633, 0.1944628 最小/最大(类似于年龄,但包括经纬度)

现在我们将向表中插入一些数据。我们将插入三位客户,vector 值可能会使 [email protected] 客户的特点如:35岁男性,住在伦敦附近:

sql 复制代码
INSERT INTO customer (email, vector) VALUES
  ('[email protected]', '[0.1259227, 0, 0, 0, 1, 0.0318873, 0.4660917]'),
  ('[email protected]', '[0.2331882, 0, 0, 0, 1, 0.0345919, 0.3901348]'),
  ('[email protected]', '[0.8177326, 0, 0, 1, 0, 0.7825610, 0.6627843]');

现在我们将运行一个距离查询,返回与 [email protected] 客户紧密相关的客户:

vbnet 复制代码
SELECT
  email,
  vector <-> '[0.1259227, 0, 0, 0, 1, 0.0318873, 0.4660917]' AS distance
FROM customer
ORDER BY distance;

结果为:

java 复制代码
[email protected] | 0
[email protected] | 0.13146349255472864
[email protected] | 1.7552207999159306

得分越低,客户的 vector 列与正在测试的向量越接近。因此,[email protected] 客户(我们用来进行比较的向量)得分为 0;这是最接近的距离。[email protected] 客户也有一个低的距离分数,意味着他们的向量特征与正在测试的特征非常接近;也许他们喜欢的产品对 [email protected] 客户也有相关性?

向量的支持还意味着 CockroachDB 可以用作检索增强生成(RAG)的后端。这允许你现有的 CockroachDB 数据集增强 LLM 的输出,训练模型并从数据中提取更多价值。有关演示,请参见"CockroachDB for AI/ML: LLMs and RAG"。

CREATE TABLE AS SELECT

CREATE TABLE AS SELECT 子句允许我们创建一个新表,该表具有 SQL SELECT 语句的数据和属性。可以为现有表指定列、约束和索引,但它们必须与 SELECT 语句返回的数据类型和列数对齐。例如,这里我们基于 movr 数据库中的两个表进行 JOIN 和聚合来创建一个新表:

vbnet 复制代码
CREATE TABLE user_ride_counts AS
SELECT u.name, COUNT(u.name) AS rides
  FROM "users" AS u JOIN "rides" AS r
    ON (u.id = r.rider_id)
 GROUP BY u.name;

请注意,虽然 CREATE TABLE AS SELECT 可用于创建汇总表等,但 CREATE MATERIALIZED VIEW 提供了更具功能性的替代方案。

修改表

ALTER TABLE 语句允许添加、修改、重命名或删除表的列或约束,还允许进行约束验证和分区。图4-10展示了语法。

在线修改表结构不是一项轻松的任务,尽管 CockroachDB 提供了高度先进的机制,可以在不影响可用性且对性能影响最小的情况下传播这些更改。我们将在后续章节中讨论在线模式更改的过程。

ALTER TABLE 语句的概述很大,因此图4-10展示了一个简化的表示,列出了可用的命令。有关完整的概述,请参见 ALTER TABLE 文档。

删除表

可以使用 DROP TABLE 语句删除表。图4-11展示了语法。

可以使用单个 DROP TABLE 语句删除多个表。CASCADE 关键字会导致与表相关的依赖对象(如视图或外键约束)也被删除。RESTRICT(默认选项)具有相反的效果;如果存在任何依赖对象,则不会删除该表。

DROP CASCADE 和 外键约束

DROP TABLE...CASCADE 会删除任何引用该表的外键约束,但不会删除包含这些外键的表或行。最终的结果是这些表中会留下"悬空"引用。

由于这种不完整性,而且因为很难准确确定 CASCADE 会做什么,通常最好在删除表之前手动删除所有依赖于该表的对象。

视图

标准视图是存储在数据库中的查询定义,定义了一个虚拟表。这个虚拟表可以像常规表一样引用。公共表表达式(Common Table Expressions)可以看作是为单个SQL语句创建临时视图的一种方式。如果你有一个希望在多个SQL语句中共享的公共表表达式,那么使用视图将是一个合乎逻辑的解决方案。

物化视图将视图定义的结果存储在数据库中,这样在每次遇到该视图时就不需要重新执行视图查询。这提高了性能,但可能会导致结果过时。如果将视图视为一个存储的查询,那么物化视图可以被看作是存储的结果。

图4-12展示了 CREATE VIEW 语句的语法。

REFRESH MATERIALIZED VIEW 语句可以用来刷新物化视图底层的数据。

函数

用户定义的函数(UDFs)是数据库中的命名函数,可以在 SELECTFROMWHERE 子句中调用。它们可以接受零个或多个参数,返回一个值,并且可以用 SQL 或 PL/pgSQL 表达。

图4-13展示了 CREATE FUNCTION 语句的语法。

在以下示例中,创建了一个接受零个参数并返回一个整数的函数。请注意,因为该函数不会修改数据,并且保证每次调用时返回相同的结果,我们可以使用 IMMUTABLELEAKPROOF 注释来装饰它:

sql 复制代码
CREATE FUNCTION add(a INT, b INT) RETURNS INT IMMUTABLE LEAKPROOF LANGUAGE SQL
AS 'SELECT $1 + $2';

该函数可以通过以下 SELECT 语句进行调用:

csharp 复制代码
SELECT easy_as();
-- 返回 123

SELECT easy_as() * 2;
-- 返回 246

函数的 波动性 表示在调用函数时是否应期望副作用。前面的函数仅返回一个整数,没有副作用,因此成本优化器可以将其内联,从而提高性能。

以下函数稍微复杂一些,它使用了参数和 PL/pgSQL 语言。注意,IMMUTABLELEAKPROOF 注释不再存在,这使得该函数具有波动性。对于相同的参数,这个函数在调用之间可能返回不同的数据(例如,产品被添加、编辑或删除):

sql 复制代码
CREATE OR REPLACE FUNCTION products_in_range(lo DECIMAL, hi DECIMAL)
RETURNS JSONB AS $$
DECLARE
  result JSONB;
BEGIN
  SELECT jsonb_agg(jsonb_build_object(
    'name', name,
    'price', price
  ))
  INTO result
  FROM product
  WHERE price BETWEEN lo AND hi;

  RETURN COALESCE(result, '[]'::JSONB);
END;
$$ LANGUAGE plpgsql;

让我们创建并填充一个表来展示这个函数是如何工作的:

sql 复制代码
CREATE TABLE product (
  "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  "name" STRING NOT NULL,
  "price" DECIMAL NOT NULL
);

INSERT INTO product ("name", "price") VALUES
  ('a', 1.99),
  ('b', 2.99),
  ('c', 4.99),
  ('d', 8.99);

使用不同的低价和高价参数调用该函数会得到以下结果:

scss 复制代码
SELECT products_in_range(2, 5);
-- 返回 [{"name": "b", "price": 2.99}, {"name": "c", "price": 4.99}]

SELECT products_in_range(10, 100);
-- 返回 []

存储过程

存储过程与函数类似,能够接受零个或多个参数,但它们不返回值、不接受波动性注释,并且是通过 CALL 而不是 SELECT 来调用的。当你希望在数据库内实现复杂的逻辑,而不是在应用程序中实现这些逻辑时,存储过程非常有用。

图4-14展示了 CREATE PROCEDURE 语句的语法。

在以下示例中,创建了一个存储过程,它通过应用新添加的"税"列中指定的金额来更新 product 表中的价格。该过程将按批次更新表中的数据,直到所有产品价格更新完毕,此时存储过程完成。

首先,我们将更新 product 表,添加新的"税"列:

sql 复制代码
ALTER TABLE product ADD tax DECIMAL NOT NULL DEFAULT 20;

接下来,我们将创建存储过程本身(请注意,现实中你不应该以这种方式更新产品价格;这个存储过程仅用于演示):

sql 复制代码
CREATE OR REPLACE PROCEDURE apply_tax(new_tax DECIMAL, batch_size INT DEFAULT 1)
LANGUAGE plpgsql AS $$
DECLARE
  updated_ids   UUID;
BEGIN
  LOOP
    UPDATE product
    SET
      price = ROUND(price / (1 + tax / 100) * (1 + new_tax / 100), 2),
      tax = new_tax
    WHERE tax != new_tax
    LIMIT batch_size
    RETURNING id INTO updated_ids;

    EXIT WHEN updated_ids IS NULL;
  END LOOP;
END;
$$;

最后,我们将调用该存储过程两次,第一次将税率增加到 30%,然后再次将其降低到 20%:

sql 复制代码
CALL apply_tax(30, 1000);
SELECT "name", "price", "tax" FROM product ORDER BY name;
  name | price | tax
-------+-------+------
  a    |  2.16 |  30
  b    |  3.24 |  30
  c    |  5.41 |  30
  d    |  9.74 |  30
sql 复制代码
CALL apply_tax(20, 1000);
SELECT "name", "price", "tax" FROM product ORDER BY name;
  name | price | tax
-------+-------+------
  a    |  1.99 |  20
  b    |  2.99 |  20
  c    |  4.99 |  20
  d    |  8.99 |  20

插入数据

我们可以通过 INSERT 语句将数据加载到新表中,可以在程序内部或从命令行 Shell 使用,也可以通过前面讨论过的 CREATE TABLE AS SELECT 语句,或者通过使用 IMPORT 语句加载外部数据。此外,还有一些非 SQL 工具可以插入数据------我们将在第7章中探讨这些工具。

传统的 INSERT 语句用于向现有表中添加数据。图4-15展示了 INSERT 语句的简化语法。

INSERT 语句可以接受一组值或一个 SELECT 语句。例如,在以下示例中,我们向 people 表插入一行数据:

sql 复制代码
INSERT INTO people (firstName, lastName, dateOfBirth)
VALUES ('Guy', 'Harrison', '21-JUN-1960');

INSERT 语句的 VALUES 子句可以接受多个值,从而在一次执行中插入多行数据:

sql 复制代码
INSERT INTO people (firstName, lastName, dateOfBirth)
VALUES ('Guy', 'Harrison', '21-JUN-1960'),
       ('Michael', 'Harrison', '19-APR-1994'),
       ('Oriana', 'Harrison', '18-JUN-2020');

在各种程序语言驱动程序中,有不同的方式可以批量插入数据,我们将在第7章中展示一些示例。

SELECT 语句可以指定为插入数据的来源:

sql 复制代码
INSERT INTO people (firstName, lastName, dateOfBirth)
SELECT firstName, lastName, dateOfBirth
  FROM peopleStagingData;

RETURNING 子句允许将插入的数据返回给用户。返回的数据不仅包括插入的变量,还包括任何自动生成的数据。例如,在这种情况下,我们在 INSERT 数据时没有指定 ID 值,并且返回了创建的 ID 值:

sql 复制代码
INSERT INTO people (firstName, lastName, dateOfBirth)
VALUES ('Guy', 'Harrison', '21-JUN-1960'),
       ('Michael', 'Harrison', '19-APR-1994'),
       ('Oriana', 'Harrison', '18-JUN-2020')
  RETURNING id;

ON CONFLICT 子句允许您控制如果 INSERT 违反唯一性约束时会发生什么。图4-16展示了语法。

没有 ON CONFLICT 子句时,违反唯一性约束会导致整个 INSERT 语句中止。DO NOTHING 允许 INSERT 语句作为整体成功,但会忽略任何违反唯一性约束的插入。DO UPDATE 子句允许你指定一个 UPDATE 语句,该语句将在插入失败时执行。DO UPDATE 的功能类似于本章稍后讨论的 UPSERT 语句。

更新数据

UPDATE 语句用于更改表中的现有数据。图4-17展示了 UPDATE 语句的简化语法。

UPDATE 语句可以指定静态值,如下例所示:

ini 复制代码
UPDATE users
  SET address = '201 E Randolph St',
      city = 'Amsterdam'
 WHERE name = 'Maria Weber';

或者,值可以是引用现有值的表达式:

ini 复制代码
UPDATE user_promo_codes
   SET usage_count = usage_count + 1
  WHERE user_id = '297fcb80-b67a-4c8b-bf9f-72c404f97fe8';

也可以使用子查询来获取更新值:

sql 复制代码
UPDATE rides SET (revenue, start_address) =
    (SELECT revenue, end_address FROM rides
      WHERE id = '94fdf3b6-45a1-4800-8000-000000000123')
 WHERE id = '851eb851-eb85-4000-8000-000000000104';

RETURNING 子句可用于查看已修改的列。这在通过函数更新列时特别有用,我们希望将修改后的值返回给应用程序:

ini 复制代码
UPDATE user_promo_codes
   SET usage_count = usage_count + 1
  WHERE user_id = '297fcb80-b67a-4c8b-bf9f-72c404f97fe8'
 RETURNING (usage_count);

UPSERT

UPSERT 可以在一次操作中插入新数据并更新表中的现有数据。如果输入数据不违反任何唯一性约束,它会被插入。如果输入数据匹配现有的主键,则该行的值将被更新。

在 CockroachDB 中,INSERTON CONFLICT 子句提供了类似的机制------虽然更加灵活。当不需要这种灵活性时,UPSERT 通常比类似的 INSERT...ON CONFLICT DO UPDATE 语句更快。

图4-18展示了 UPSERT 语句的语法。

UPSERT 比较每行提供的主键 KV。如果在现有表中找不到主键,则会创建一行新数据;否则,现有行将使用提供的新值进行更新。

RETURNING 子句可用于返回已更新或插入的行列表。

在这个例子中,user_promo_codes 表的主键是 (city, user_id, code)。如果用户已经在表中有该组合的条目,则该行会更新 user_count 为 0;否则,创建一行具有这些值的新数据:

sql 复制代码
UPSERT INTO user_promo_codes
  (user_id, city, code, timestamp, usage_count)
SELECT id, city, 'NewPromo', now(), 0
  FROM "users";

删除数据

DELETE 语句允许从表中删除数据。图4-19展示了 DELETE 语句的简化语法。

大多数时候,DELETE 语句接受一个 WHERE 子句,其他的则不常用。例如,在以下示例中,我们删除 people 表中的一行数据:

ini 复制代码
DELETE FROM people
 WHERE firstName = 'Guy'
   AND lastName = 'Harrison';

RETURNING 子句可以返回已删除行的详细信息。例如:

ini 复制代码
DELETE FROM user_promo_codes
 WHERE code = 'NewPromo'
RETURNING(user_id);

您还可以包含 ORDER BYLIMIT 子句,以增量的方式执行批量删除。例如,您可以构造一个 DELETE 语句来删除最旧的 1,000 行。有关更多信息,请参见 CockroachDB 文档。

TRUNCATE

TRUNCATE 提供了一种快速的机制,用于删除表中的所有行。在内部,它实现为先执行 DROP TABLE 然后执行 CREATE TABLETRUNCATE 不是事务性的------您无法对 TRUNCATE 执行 ROLLBACK 操作。

IMPORT INTO

IMPORT INTO 语句将以下类型的数据导入现有的 CockroachDB 表:

  • Avro
  • 逗号分隔值(CSV)/制表符分隔值(TSV)

要导入的文件应位于云存储桶(如 Google Cloud Storage、Amazon S3 或 Azure Blob 存储)、HTTP 地址或本地文件系统(nodelocal)中。

我们将在第7章中讨论将数据加载到 CockroachDB 中的各种选项。不过,现在,让我们从 CSV 文件创建一个新的 customers 表:

sql 复制代码
IMPORT INTO TABLE customers (
        id INT PRIMARY KEY,
        name STRING,
        INDEX name_idx (name)
);
CSV DATA ('nodelocal://1/customers.csv');

输出结果:

sql 复制代码
        job_id       |  status   | fra | rows | index_entries | bytes
---------------------+-----------+-----+------+---------------+--------
  659162639684534273 | succeeded |   1 |    1 |             1 |    47
(1 row)

Time: 934ms total (execution 933ms / network 1ms)

对于单节点演示集群,nodelocal 位置将依赖于您的安装,但通常会位于 CockroachDB 安装目录下的 extern 目录中。

事务语句

我们在第2章中详细讨论了 CockroachDB 事务,如果您需要回顾 CockroachDB 事务如何工作,可以查看该章节。从 SQL 语言的角度来看,CockroachDB 支持标准的 SQL 事务控制语句。

BEGIN 事务

BEGIN 语句启动一个事务并设置其属性。图4-20展示了语法。

PRIORITY 设置事务的优先级。在发生冲突时,优先级为 HIGH 的事务更不容易被重试。

READ ONLY 指定该事务是只读的,并且不会修改数据。

AS OF SYSTEM TIME 允许一个只读事务查看数据库历史的快照数据。我们将在后续几页中详细讨论这个功能。

SAVEPOINT

SAVEPOINT 创建一个命名的回滚点,可以作为 ROLLBACK 语句的目标。这使得事务的一部分可以被丢弃,而不丢弃整个事务的工作。有关更多细节,请参见 ROLLBACK 部分。

COMMIT

COMMIT 语句提交当前事务,使更改永久生效。

请注意,一些事务可能需要客户端干预来处理重试场景。这些模式将在第6章中探讨。

ROLLBACK

ROLLBACK 会中止当前事务。可以选择回滚到一个保存点,这样只会回滚在 SAVEPOINT 之后执行的语句。

例如,在以下示例中,插入拼写错误的数字 tree 会被回滚并纠正,而不放弃整个事务:

sql 复制代码
BEGIN;

INSERT INTO numbers VALUES(1,'one');
INSERT INTO numbers VALUES(2,'two');
SAVEPOINT two;

INSERT INTO numbers VALUES(3,'tree');
ROLLBACK TO SAVEPOINT two;

INSERT INTO numbers VALUES(3,'three');
COMMIT;

SELECT FOR UPDATE

SELECT 语句的 FOR UPDATE 子句会锁定查询返回的行,确保在读取和事务结束之间,其他事务不能修改这些行。这通常用于实现我们将在第6章讨论的悲观锁定模式。

FOR UPDATE 查询应在事务内执行,否则,锁会在 SELECT 语句执行完毕后被释放。

在事务内发出的 FOR UPDATE 默认会阻止其他对相同行的 FOR UPDATE 语句或其他试图更新这些行的事务执行,直到发出 COMMITROLLBACK。但是,如果一个优先级较高的事务尝试更新这些行或尝试发出 FOR UPDATE,则低优先级的事务将被中止,并需要重试。

我们将在第6章中讨论事务重试的机制。

图4-21展示了两个 FOR UPDATE 语句并发执行的情形。第一个 FOR UPDATE 会对受影响的行保持锁定,防止第二个会话获取这些锁,直到第一个会话完成其事务。

SELECT FOR SHARE

SELECT 语句的 FOR SHARE 子句会在查询返回的行上获得一个共享锁,防止写入操作和其他独占锁定读取操作在事务结束之前获得锁。如果一行被多个共享锁锁定,那么这些锁的持有者都无法对该行进行写操作。

当以 READ COMMITTED 隔离级别运行时,如果你需要获取最新数据但不打算在读取数据后进行更新,应该使用 FOR SHARE

在事务内发出的 FOR SHARE 不会阻塞其他对相同行的 FOR SHARE 语句,但会阻塞其他事务对这些行的更新操作,直到发出 COMMITROLLBACK。然而,如果一个优先级较高的事务尝试更新这些行,那么低优先级的事务将被中止并需要重试。

我们将在第6章中讨论事务重试的机制。

图4-22展示了两个 FOR SHARE 语句并发执行的情形。第一个 FOR SHARE 在受影响的行上创建共享锁,允许第二个会话获取共享锁,但在第一个会话完成其事务之前,第二个会话无法获得独占锁。

AS OF SYSTEM TIME

AS OF SYSTEM TIME 子句可以应用于 SELECT 语句、BEGIN TRANSACTION 语句,以及在 BACKUPRESTORE 操作中。AS OF SYSTEM TIME 指定一个 SELECT 语句或所有 READ ONLY 事务中的语句应该在某个系统时间的数据库快照上执行。这些快照是通过第2章中描述的 MVCC 架构提供的。

时间可以通过偏移量或绝对时间戳来指定,如以下两个示例所示:

sql 复制代码
SELECT * FROM rides r
   AS OF SYSTEM TIME '-1d';

SELECT * FROM rides r
   AS OF SYSTEM TIME '2024-5-22 18:02:52.0+00:00';

指定的时间不能早于 gc.ttlseconds(复制区配置参数)所控制的 MVCC 快照最大年龄,以秒为单位。

还可以使用 with_max_staleness 参数来指定有界陈旧读取:

sql 复制代码
SELECT * FROM rides r
    AS OF SYSTEM TIME with_max_staleness('10s')
 WHERE city='amsterdam'
   AND id='aaaae297-396d-4800-8000-0000000208d6';

有界陈旧读取可以通过允许 CockroachDB 从本地副本读取可能略显陈旧的数据来优化分布式部署的性能。我们将在第11章进一步探讨有界陈旧读取。

其他数据定义语言目标

到目前为止,我们已经讨论了如何使用 SQL 来创建、修改和操作表格和索引数据。这些对象代表了 CockroachDB 中数据库功能的核心,和其他 SQL 数据库一样。然而,CockroachDB 的数据定义语言 (DDL) 还提供了对许多其他较少使用的对象的支持。对于这些对象的完整参考将在这里占用更多空间------请参阅 CockroachDB 文档以获取完整的 CockroachDB SQL 列表。表 4-5 列出了一些可以在 CREATE、ALTER 和 DROP 语句中操作的其他对象。

表 4-5. 其他 CockroachDB 架构对象

对象 描述
数据库 数据库是 CockroachDB 集群中的命名空间,包含模式、表、索引和其他对象。数据库通常用于分隔具有不同应用职责或安全策略的对象。
模式 模式是属于同一关系模型的一组表和索引。在大多数数据库中,表默认在 PUBLIC 模式中创建。
序列 序列常用于创建主键 KV;然而,在 CockroachDB 中,通常有更好的替代方案。有关主键生成的更多指导,请参见第 5 章。
角色 角色用于组合数据库和模式权限,然后可以作为一个单元授予用户。有关 CockroachDB 安全实践的更多细节,请参见第 12 章。
类型 在 CockroachDB 中,类型是一组可以应用于 CREATE 或 ALTER TABLE 语句中列的枚举值。
用户 用户是一个账户,用于登录数据库,并可以分配特定的权限。有关 CockroachDB 安全实践的更多细节,请参见第 12 章。
统计信息 统计信息是 SQL 优化器用来计算最佳执行计划的表中的数据。有关查询优化的更多信息,请参见第 8 章。
变更提要 变更提要流式传输指定表的行级变化到客户端程序。有关变更提要实现的更多信息,请参见第 7 章。
调度 调度控制备份的周期性执行。有关备份策略的指导,请参见第 11 章。
函数 用户定义的函数。
存储过程 用户定义的存储过程。

管理命令

CockroachDB 支持管理命令,用于维护用户的身份验证及其执行数据库操作的权限。它还具有作业调度程序,可以用来调度备份和恢复操作以及定期的架构变更。其他命令支持集群拓扑的维护。

这些命令通常与特定的管理操作紧密耦合,我们将在后续章节中讨论,因此这里不再详细定义它们。您可以随时在 CockroachDB 文档中查看它们的定义。表 4-6 概述了这些命令中最重要的一些。

表 4-6. CockroachDB 管理命令

命令 描述
CANCEL JOB 取消长时间运行的作业,如备份、架构更改或统计信息收集。
CANCEL QUERY 取消当前正在运行的查询。
CANCEL SESSION 取消并断开当前连接的会话。
CONFIGURE ZONE 使用 CONFIGURE ZONE 可以修改表、数据库、范围或分区的复制区域。有关区域配置的更多信息,请参见第 10 章。
SET CLUSTER SETTING 更改集群配置参数。
EXPLAIN 显示 SQL 语句的执行计划。我们将在第 8 章中详细讨论 EXPLAIN。
EXPORT 将 SQL 输出转储到 CSV 文件中。
SHOW/CANCEL/PAUSE JOBS 管理数据库中的后台作业------导入、备份、架构更改等。
SET LOCALITY 更改多区域数据库中表的局部性。有关更多信息,请参见第 10 章。
SET TRACING 为会话启用跟踪功能。我们将在第 8 章中讨论此内容。
SHOW RANGES 显示表、索引或数据库如何分割为范围。有关 CockroachDB 如何将数据拆分为范围的讨论,请参见第 2 章。
SPLIT AT 强制在表或索引中的指定行进行范围拆分。
BACKUP 为表或数据库创建一致的备份。有关备份和高可用性的指导,请参见第 11 章。
SHOW STATISTICS 显示表的优化器统计信息。
SHOW TRACE FOR SESSION 显示由 SET TRACING 命令创建的会话跟踪信息。
SHOW TRANSACTIONS 显示当前运行的事务。
SHOW SESSION 显示本地节点或整个集群中的会话。

信息模式

信息模式是每个数据库中的特殊模式,包含关于数据库中其他对象的元数据------在 CockroachDB 中,它被命名为 INFORMATION_SCHEMA。您可以使用信息模式发现数据库中对象的名称和类型。例如,您可以使用信息模式列出 information_schema 模式中的所有对象:

sql 复制代码
SELECT * FROM information_schema."tables"
 WHERE table_schema='information_schema';

或者,您可以使用 information_schema 来显示表中的列:

ini 复制代码
SELECT column_name, data_type, is_nullable, column_default
 FROM information_schema.COLUMNS WHERE TABLE_NAME='customers';

信息模式对于编写针对未知数据模型的应用程序特别有用。例如,像 DBeaver 这样的 GUI 工具使用信息模式来填充数据库树并显示表和索引的信息。

信息模式是 ANSI 标准定义的,并被许多关系型数据库实现。CockroachDB 还包括一些特定于 CockroachDB 系统的内部表,这些表位于 crdb_internal 模式中。有关这些表的信息可以在 CockroachDB 文档中找到。

总结

在本章中,我们回顾了 CockroachDB 数据库中用于创建、查询和修改数据的 SQL 语言基础。

CockroachDB SQL 的所有语法元素的完整定义需要一本书来详细描述,因此我们主要集中讨论了 SQL 语言的核心功能,并着重介绍了 CockroachDB 特有的功能。有关详细的语法和 CockroachDB 管理命令的详细信息,请参阅 CockroachDB 在线文档。

SQL 是 CockroachDB 的语言,因此我们将在深入探索 CockroachDB 的世界时继续阐述 CockroachDB SQL 语言。

在下一章中,我们将探讨架构设计以及 CockroachDB 中能够支持高效和高性能查询的功能。

相关推荐
uhakadotcom10 小时前
视频直播与视频点播:基础知识与应用场景
后端·面试·架构
沉登c13 小时前
第 3 章 事务处理
架构
数据智能老司机16 小时前
CockroachDB权威指南——开始使用
数据库·分布式·架构
松果猿16 小时前
空间数据库学习(二)—— PostgreSQL数据库的备份转储和导入恢复
数据库
c无序16 小时前
【Docker-7】Docker是什么+Docker版本+Docker架构+Docker生态
docker·容器·架构
无名之逆17 小时前
Rust 开发提效神器:lombok-macros 宏库
服务器·开发语言·前端·数据库·后端·python·rust
s91236010117 小时前
rust 同时处理多个异步任务
java·数据库·rust
数据智能老司机17 小时前
CockroachDB权威指南——CockroachDB 架构
数据库·分布式·架构
IT成长日记17 小时前
【Kafka基础】Kafka工作原理解析
分布式·kafka