非规范化表和重复数据

Posted

技术标签:

【中文标题】非规范化表和重复数据【英文标题】:Denormalized table and duplicated data [closed] 【发布时间】:2018-01-01 13:19:24 【问题描述】:

我的 Java 项目使用纯 JDBC 与 Oracle DB (v. 12) 交互。事务隔离级别为 Read Committed。

我有一个高度非规范化的表,它将一个实体存储在一组行中。我无法改变这一点。不幸的是,这张桌子必须保持这种状态,原因与我无关。

+------+------+---------+
| date | hash | ....... |
+------+------+---------+
| date | xyz  | ....... |
| date | xyz  | ....... |
| date | xyz  | ....... |

我有两列标识一个实体 - 一个日期和一个哈希。由于每个实体都存储为多行,因此这些列并不是真正唯一的,也不是主键,而只是索引列。我仍然想强制执行一种“唯一性”,这意味着当时只存在一个实体,无论它由多少行组成。

这样的实体一天可以更新几次,产生不同的值,但也有不同的行数。

为了实现这一切,每次更新实体时,我都会在单个事务中执行两个或多个查询:

delete from "table" where "date" = ? and "hash" = ?
insert into "table" values (?, ?, .....)
insert into "table" ....
... -- as many inserts as needed to store whole entity

这适用于单个应用程序实例。不幸的是,我有 2 个实例同时工作,试图几乎同时存储 完全相同的数据(它们只是主备份实例,但备份也在持续存在 - 这我也没有影响)。

如果这是规范化的表,解决方案是使用 MERGE 语句,但在这里不起作用。

我目前的解决方案:

到目前为止,我尝试做的是再添加一列,实例的 ID 持久化,然后使用 SELECT 作为数据源执行 INSERT 语句,并将条件设置为 SELECT,该日期/哈希必须没有数据和应用 ID,否则 SELECT 不提供要插入的数据。

我认为它会起作用,但显然它不起作用。我仍然看到重复。我认为这是因为两个事务首先进行了删除,仍然看不到其他事务尚未提交的数据,因此自己执行插入。然后“提交”是执行和繁荣。两个事务都插入它们的数据。

我考虑过的其他方法:

我想乐观锁定也不起作用,因为在最终版本检查中,两个事务仍然可以认为版本没有被更改,而它们实际上同时被两个事务同时更改并且即将以这种方式提交.

我知道我可以将事务隔离切换到 SERIALIZABLE,但它也不是完美的(首先,Oracle 驱动程序不会序列化查询,但会采取乐观的方法并在并发修改的情况下失败并出现错误,我不这样做'不喜欢这样,它是一种“异常编程”范式,一种反模式,那么第二个缺点当然是性能)。

对于这样的问题还有其他解决方案吗?

【问题讨论】:

拥有多个应用程序实例并不一定意味着您可以(或应该)拥有多个数据库实例。我认为最简单的解决方案是只拥有一个数据库。 你已经有这么多反模式,声称你不能使用 SERIALIZABLE 因为它是一个反模式是可笑的。 INSERT 语句是否总是插入具有完全相同日期和哈希值的记录,然后 DELETE 会删除这些记录?或者,DELETE 删除具有一个日期/哈希的记录(例如 2017-12-12/123),但 INSERT 在同一事务中插入其他值(例如 2017-12-15/321)? @Kayaman:表格的设计方式不取决于我,但必须处理它的代码由我完成,这部分我想尽可能避免反模式尽可能。 @krokodilko:DELETE 删除与插入时相同的日期/哈希。这是一种对任意数量的记录进行 UPDATE 的方法(要求更新前后的记录数量可能会发生变化)。除非您知道更好的方法吗? 【参考方案1】:

为了序列化这两个事务,我会创建一个额外的表:

CREATE TABLE locktable(
  my_date date,
  my_hash number,
  primary key (my_date, my_hash)
);

并会通过以下方式更改整个交易:

INSERT INTO locktable( my_date, my_hash ) VALUES ( date_value, hash_value );

delete from "table" where "date" = date_value and "hash" = hash_value;
insert ....
insert ....

DELETE FROM locktable WHERE my_date = date_value AND my_hash = hash_value;
COMMIT;

第一个 INSERT 语句将序列化事务,因为现有的主键约束会阻止向表中插入两条重复记录。 您可以通过使用两个不同的会话和默认隔离级别 READ COMMITED 运行一个简单的测试来了解它是如何工作的。


首先,让我们创建测试数据:

CREATE TABLE my_table(
  my_date date,
  my_hash number,
  somevalue int
);

INSERT INTO my_table( my_date, my_hash, somevalue)
SELECT trunc( sysdate ), 123, 111 FROM dual
CONNECT BY level <= 3;
commit;

CREATE TABLE locktable(
  my_date date,
  my_hash number,
  primary key (my_date, my_hash)
);

会话 #1 - 查看原始数据。 我们将向 locktable 中插入一条记录,然后删除旧记录并插入新记录。

SQL> select * from my_table;

MY_DATE      MY_HASH  SOMEVALUE
--------- ---------- ----------
01-JAN-18        123        111
01-JAN-18        123        111
01-JAN-18        123        111

SQL> INSERT INTO locktable( my_date, my_hash ) VALUES ( trunc( sysdate), 123 );

1 row created.

SQL> DELETE FROM my_table WHERE my_date = trunc( sysdate ) AND my_hash = 123;

3 rows deleted.

SQL> INSERT INTO my_table( my_date, my_hash, somevalue)
  2  SELECT trunc( sysdate ), 123, 222 FROM dual CONNECT BY level <= 3;

3 rows created.

会话 #2 - 此会话看不到会话 #1 插入的记录,因为它尚未提交 (somevalue = 111):

SQL> select * from my_table;

MY_DATE      MY_HASH  SOMEVALUE
--------- ---------- ----------
01-JAN-18        123        111
01-JAN-18        123        111
01-JAN-18        123        111

SQL> INSERT INTO locktable( my_date, my_hash ) VALUES ( trunc( sysdate), 123 );

当执行 INSERT 时,会话 #2 “挂起”(处于保持状态),因为 Oracle 检测到表 locktable 中存在由另一个会话插入的重复记录,该记录尚未提交. Oracle 现在将等待第一个会话将执行的操作:

如果第一个会话将执行 COMMIT,则重复错误将是 在会话 #2 中抛出 如果第一个会话将执行 ROLLBACK,或者将删除该行并执行 COMMIT,则 sessin #2 将被解锁并插入该行

让我们进入会话#1并做:

SQL> DELETE FROM  locktable WHERE my_date = trunc( sysdate) AND my_hash = 123;

1 row deleted.

SQL> commit;

Commit complete.

现在让我们看看第 2 次会议发生了什么:

SQL> INSERT INTO locktable( my_date, my_hash ) VALUES ( trunc( sysdate), 123 );

1 row created.

SQL>

会话已解除阻止并继续工作。 让我们再做一次检查:

SQL> select * from my_table;

MY_DATE      MY_HASH  SOMEVALUE
--------- ---------- ----------
01-JAN-18        123        222
01-JAN-18        123        222
01-JAN-18        123        222

现在会话 #2 看到会话 #1 提交的更改 !!! 这是因为在Read Committed Isolation Level:

在读提交隔离级别,这是默认的,每个 由事务执行的查询只看到之前提交的数据 查询——不是交易——开始。这种隔离级别是 适用于事务很少的数据库环境 可能会发生冲突。

也就是说 - 第一个事务提交,然后第二个事务被解除阻塞,然后第二个事务看到第一个事务所做的更改,尽管第二个事务比第一个事务开始晚。


现在我们可以继续处理第二个事务(删除旧数据并插入新数据)。如果另一个(第三个)事务开始(具有相同的日期和哈希),由于locktable 表中的现有记录,它将再次被搁置。


上述方法将确保仅此一笔交易的正确序列化。 如果应用程序也在其他地方插入或删除记录,除非其他地方相应更改,否则将无法正常工作。

【讨论】:

这是另一种有趣的方法。虽然我相信它会从根本上解决问题,但它需要另一个表,就我而言,这可能是有问题的。不过,这是一个很好的解决方案。我赞成它,但我认为我不会使用它。谢谢!【参考方案2】:

据我阅读,您的要求是:

数据库结构不能改变 两个应用程序必须同时更新完全相同的数据 乐观锁定已失效,因为它可能会导致错误或性能下降 悲观锁定与乐观锁定的原因相同

似乎最重要的不是您要更改什么数据,而是您正在读取什么数据。您需要一种方法来确定应该为您的系统用户提供哪些数据(我不知道这些应用程序是只是维护数据还是同时使用数据)。

我假设您当前对提供数据的查询类似于:

select * from table where date = :1 and hash = :2

如果您将其更改为以下内容,那么您将始终选择最新数据,如果及时出现重复数据,您将选择第一个应用程序(基本上是随机的 - 更改为您想要的任何顺序)

select *
  from ( select t.*
              , rank() over (partition by hash 
                                 order by date desc, app_id desc) as rnk
           from table t
                )
 where rnk = 1

你可以把它放在一个视图中吗?

然后,您基本上可以在一个表中运行两个单独的表。您可以使用 MERGE 等,并且可以将您的 DELETE/INSERT 语句更改为:

merge into table o
using (select :1, :2 ... ) n
   on ( o.date = n.date
       and o.hash = n.hash
       and o.app_id = n.app_id
           )
 when matched then
      update
         set ...
 when not matched then
      insert (...

commit;

delete from table
 where date < :1 
   and hash = :2

commit;

您在 MERGE 语句中使用相同的日期和哈希值。如果 DELETE 失败,您并不介意 - 因为您更改了 SELECT 查询,所以您不会选择错误的数据。


就我个人而言,我承认您的其中一项要求必须改变。

如果有任何添加其他应用程序的计划,我会接受性能下降并使用排队机制在此表上连续执行更新。

如果没有添加其他应用程序的计划,请立即采用简单的方法并开始使用锁定策略(不是很漂亮)并处理一些已知错误。

【讨论】:

这似乎是正确的方法(处理阅读,而不是写作)。无论如何,我想知道您提到的合并语句。我认为它不会起作用,因为 MERGE 只希望更改现有行而不更改它们的数量,而在我的情况下,行数可以改变。我说的对吗? 合并不会删除您不再需要的数据。它将使用WHEN NOT MATCHED 部分插入新数据。之后您仍然需要执行 DELETE,但通过关注您正在阅读的数据,您甚至可以根据需要异步清除它。

以上是关于非规范化表和重复数据的主要内容,如果未能解决你的问题,请参考以下文章

动态增加数量的可行性。数据库中的表和行 [重复]

将非规范化数据加载到数据仓库中

在所有字段、表和数据库中搜索字符串的 MySQL 工具 [重复]

在所有字段、表和数据库中搜索字符串的 MySQL 工具 [重复]

如何在 Firebase 中写入非规范化数据

如何在Firebase中编写非规范化数据