非规范化表和重复数据
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 现在将等待第一个会话将执行的操作:
让我们进入会话#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 工具 [重复]