3

问题摘要

这是一个关于 SQL 事务中查询的可序列化性的问题。

具体来说,我使用的是 PostgreSQL。可以假设我使用的是最新版本的 PostgreSQL。根据我的阅读,我相信用于支持我正在尝试做的事情的技术被称为“多版本并发控制”或“MVCC”。

总结一下:如果我有一个主表,并且有多个外键链接表连接到该主表,我如何保证,对于表中的给定键,以及任意数量的 SELECT 语句在一个事务中使用该键,每个事务都从任何链接表中进行选择,我将获得启动事务时存在的数据?

其他问题

这个问题类似,但范围更广,而且问题和答案并没有专门与 PostgreSQL 相关: Transaction isolation and reading from multiple tables on SQL Server Express and SQL Server 2005

例子

假设我有 3 张桌子:

bricks
    brickworks (primary key)
    completion_time (primary key)
    has_been_sold

brick_colors
    brickworks (primary key, foreign key pointing to "bricks")
    completion_time (primary key, foreign key pointing to "bricks")
    quadrant (primary key)
    color

brick_weight
    brickworks (primary key, foreign key pointing to "bricks")
    completion_time (primary key, foreign key pointing to "bricks")
    weight

砖厂一次生产一块砖。它在其 4 个象限中的每个象限中制作可能具有不同颜色的砖块。

稍后有人分析砖块以确定它们的颜色组合,并将结果写入brick_colors 表。

其他人分析砖块以确定它们的重量,并将结果写入brick_weight 表。

在任何给定时间,现有的砖块可能有也可能没有记录的颜色,可能有也可能没有记录的重量。


存在一个应用程序,并且该应用程序接收到有人想要购买特定砖块的消息(此时应用程序已经通过它的砖厂/完成时间组合键知道)。

应用程序希望在它开始查询的确切时间选择砖块的所有已知属性。

如果在 MID-TRANSACTION 中添加颜色或重量信息,应用程序不想知道它。

应用程序想要执行 SEPARATE QUERIES(而不是具有多个 JOIN 到外键链接表的 SELECT,这可能会因为 brick_colors 表而返回多行)。


这个例子故意简单;如果我的示例包括 10 个外键链接表,并且它们中的许多或全部可以为同一个主键返回多行(就像 brick_colors 在上面的例子)。

尝试的解决方案

到目前为止,这是我想出的:

BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE READ ONLY ;

-- All this statement accomplishes is telling the database what rows should be returned from the present point-in-time in future queries within the transaction
SELECT DISTINCT true
FROM bricks b
LEFT JOIN brick_colors bc ON bc.brickworks = b.brickworks AND bc.completion_time = b.completion_time
LEFT JOIN brick_weight bw ON bw.brickworks = b.brickworks AND bw.completion_time = b.completion_time
WHERE b.brickworks = 'Brick-o-Matic' AND b.completion_time = '2017-02-01T07:35:00.000Z' ;

SELECT * FROM brick_colors WHERE b.brickworks = 'Brick-o-Matic' AND b.completion_time = '2017-02-01T07:35:00.000Z' ;
SELECT * FROM brick_weight WHERE b.brickworks = 'Brick-o-Matic' AND b.completion_time = '2017-02-01T07:35:00.000Z' ;

COMMIT ;

仅出于确保可序列化的目的而将第一个 SELECT 与 JOIN 一起使用似乎很浪费。

有没有其他方法可以做到这一点?

参考

PostgreSQL 并发控制

PostgreSQL 事务隔离

PostgreSQL SET TRANSACTION 语句

4

1 回答 1

5

这是您问题的本质:

我如何保证,对于......任意数量的 SELECT 语句......在一个事务中......我将获得开始事务时存在的数据?


这正是可重复读取隔离级别所保证的:

Repeatable Read 隔离级别只看到事务开始之前提交的数据;它永远不会看到未提交的数据或并发事务在事务执行期间提交的更改。(但是,查询确实会看到在其自己的事务中执行的先前更新的影响,即使它们尚未提交。)这是比 SQL 标准对此隔离级别要求的更强的保证,并且可以防止所有现象表 13-1 中描述。如上所述,这是标准明确允许的,该标准仅描述了每个隔离级别必须提供的最低保护。

此级别不同于已提交读,因为可重复读取事务中的查询看到事务开始时的快照,而不是事务中当前查询开始时的快照。因此,单个事务中的连续 SELECT 命令看到相同的数据,即,它们看不到在它们自己的事务开始后提交的其他事务所做的更改。


一个实际的例子 - 假设我们有 2 个简单的表:

CREATE TABLE t1( x int );
INSERT INTO t1 VALUES (1),(2),(3);
CREATE TABLE t2( y int );
INSERT INTO t2 VALUES (1),(2),(3);

许多表、它们的结构、主键、外键等在这里并不重要。

让我们打开第一个会话,启动可重复的读取隔离级别,然后运行两个简单且单独的 SELECT 语句:

test=# START TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION
test=# SELECT * FROM t1;
 x
---
 1
 2
 3
(3 wiersze)


test=# SELECT * FROM t2;
 y
---
 1
 2
 3
(3 wiersze)

请注意,该START TRANSACTION命令会自动禁用会话中的自动提交模式。


现在在另一个会话中(启用默认自动提交模式)将一些记录插入t1

test2=# INSERT INTO t1 VALUES(10),(11);

新值被插入并自动提交(因为自动提交已打开)。


现在回到第一个会话并再次运行 SELECT:test=# select * from t1;

 x
---
 1
 2
 3
(3 wiersze)

如您所见, session1(具有活动的可重复读取事务)在事务开始后看不到任何提交的更改。


让我们在桌子上做同样的实验t2- 转到第二个会话并发出:

test2=# DELETE FROM t2 WHERE y = 2;
DELETE 1

现在回到第一个会话并再次运行 SELECT:

test=# SELECT * FROM t2;
 y
---
 1
 2
 3
(3 wiersze)

如您所见,session1(具有活动的可重复读取事务)在事务开始后看不到任何提交的更改。


现在,在 session1 中,完成发出 COMMIT 的事务,然后 SELECT:

test=# SELECT * FROM t1;
 x
---
 1
 2
 3
(3 wiersze)

test=# SELECT * FROM t2;
 y
---
 1
 2
 3
(3 wiersze)

test=# COMMIT;
COMMIT

test=# select * from t1;
 x
----
  1
  2
  3
 10
 11
(5 wierszy)


test=# select * from t2;
 y
---
 1
 3
(2 wiersze)

如您所见,当可重复读取事务启动并处于活动状态时,您可以多次运行许多单独的 select 语句,并且所有这些 select 语句都会看到与事务开始时相同的稳定数据快照,而不管任何提交的数据在其他会话中。

于 2017-02-18T23:22:21.537 回答