前言
最近遇上一条ORACLE里的SQL,以我十多年的复杂SQL编写经验,我竟然一时半会无法用SQL语法解析出这条SQL的语义,而且这个SQL竟然是可能可以执行成功的!
delete (select * from TEST_DELETE_A a,TEST_DELETE_B b where a.id=b.id ) ;
这是两个普通的表,不是视图,也没有触发器,也没有其他会话操作这两个表。
开始实验
- 数据准备
drop table TEST_DELETE_A;
drop table TEST_DELETE_b;
CREATE TABLE TEST_DELETE_A (ID NUMBER);
INSERT INTO TEST_DELETE_A VALUES (1);
INSERT INTO TEST_DELETE_A VALUES (2);
CREATE TABLE TEST_DELETE_B AS select * from TEST_DELETE_A;
SELECT 'A' TAB,ID FROM TEST_DELETE_A
UNION ALL
SELECT 'B' TAB,ID FROM TEST_DELETE_B;
TAB ID
--- ----------
A 1
A 2
B 1
B 2
- 删除,报错 ORA-01752: 不能从没有一个键值保存表的视图中删除
delete (select * from TEST_DELETE_A a,TEST_DELETE_B b where a.id=b.id and A.id=1) ;
- 在A表增加主键,再执行DELETE,发现B表少了一条数据
alter table TEST_DELETE_A
add constraint pk_TEST_DELETE_A primary key (ID);
delete (select * from TEST_DELETE_A a,TEST_DELETE_B b where a.id=b.id and A.id=1) ;
SELECT 'A' TAB,ID FROM TEST_DELETE_A
UNION ALL
SELECT 'B' TAB,ID FROM TEST_DELETE_B;
TAB ID
--- ----------
A 1
A 2
B 2
rollback;
- 删掉A表主键,给B表增加主键,再执行DELETE,发现A表少了一条数据(与主键有关)
alter table TEST_DELETE_A
DROP constraint pk_TEST_DELETE_A;
alter table TEST_DELETE_B
add constraint pk_TEST_DELETE_B primary key (ID);
delete (select * from TEST_DELETE_A a,TEST_DELETE_B b where a.id=b.id and A.id=1) ;
SELECT 'A' TAB,ID FROM TEST_DELETE_A
UNION ALL
SELECT 'B' TAB,ID FROM TEST_DELETE_B;
TAB ID
--- ----------
A 2
B 1
B 2
rollback;
同样的delete语句,由于主键在不同表上,执行后得到了完全不同的结果!
5. 那么如果两个表都有主键呢?测试一下,发现删掉的是A表的记录
alter table TEST_DELETE_A
add constraint pk_TEST_DELETE_A primary key (ID);
delete (select * from TEST_DELETE_A a,TEST_DELETE_B b where a.id=b.id and A.id=1) ;
SELECT 'A' TAB,ID FROM TEST_DELETE_A
UNION ALL
SELECT 'B' TAB,ID FROM TEST_DELETE_B;
TAB ID
--- ----------
A 2
B 1
B 2
rollback;
6.把条件,A.id=1 改成B.id=1 ,删除的还是A表(与where条件无关)
delete (select * from TEST_DELETE_A a,TEST_DELETE_B b where a.id=b.id and B.id=1) ;
SELECT 'A' TAB,ID FROM TEST_DELETE_A
UNION ALL
SELECT 'B' TAB,ID FROM TEST_DELETE_B;
TAB ID
--- ----------
A 2
B 1
B 2
rollback;--回滚
7.把from后的表,B表放在A表前面 ,删除的就变成了B表(与顺序有关)
delete (select * from TEST_DELETE_B b,TEST_DELETE_A a where a.id=b.id and B.id=1) ;
SELECT 'A' TAB,ID FROM TEST_DELETE_A
UNION ALL
SELECT 'B' TAB,ID FROM TEST_DELETE_B;
TAB ID
--- ----------
A 1
A 2
B 2
rollback;--回滚
- 给A表增加外键 ,删除的又变回了A表(与外键有关,且外键优先级高于表的顺序)
alter table TEST_DELETE_A
add constraint FK_TEST_DELETE_A foreign key (ID)
references test_delete_b (ID);
delete (select * from TEST_DELETE_B b,TEST_DELETE_A a where a.id=b.id and B.id=1) ;
SELECT 'A' TAB,ID FROM TEST_DELETE_A
UNION ALL
SELECT 'B' TAB,ID FROM TEST_DELETE_B;
TAB ID
--- ----------
A 2
B 1
B 2
rollback;
- 给B表也增加外键,删除报错了,ORA-02292: 违反完整约束条件 (CSLX.FK_TEST_DELETE_B) - 已找到子记录 ,说明它打算删的是B表
alter table TEST_DELETE_B
add constraint FK_TEST_DELETE_B foreign key (ID)
references test_delete_a (ID);
delete (select * from TEST_DELETE_A a,TEST_DELETE_B b where a.id=b.id and A.id=1) ;
- 把A表的外键删了,再DELETE,果然删的是B表
alter table TEST_DELETE_A
drop constraint FK_TEST_DELETE_A ;
delete (select * from TEST_DELETE_A a,TEST_DELETE_B b where a.id=b.id and A.id=1) ;
SELECT 'A' TAB,ID FROM TEST_DELETE_A
UNION ALL
SELECT 'B' TAB,ID FROM TEST_DELETE_B;
TAB ID
--- ----------
A 1
A 2
B 2
rollback;
汇总测试结果
对于delete from (subquery)语法的sql,根据不同的主外键情况,会出现如下几种情况
查资料
这个问题从传统的搜索方式不知道怎么列关键字,各种组合试了几次,发现用“delete 子查询 外键 oracle”这个,能找到一个百度问答
Oracle中delete主子表关联查询结果时,为什么只会删除子表中的记录?
其实在我测试之前,我问过了NEW BING
可以看到NEW BING一开始是错的,在2次追问之后,能纠正回来,并且给出了相关资料
stackoverflow-DELETE FROM <subquery>
点赞最多的回答给了一个oracle的文档,把这个现象和对视图的DML操作联系了起来
Managing Views, Sequences, and Synonyms
里面提到了一个概念
Key-Preserved Tables
直译就是"保留键表"。
大概是这么回事,两个表有主外键关系,join后的这个查询结果,主表记录可能会重复,而外表记录不会重复,删除的时候就删有外键的那个表。假设没有外键,用主键去join时,它就认为主键连接的另一个表是外表。
这样一来,就好理解了,为什么对于同一个delete语句,可能会删除不同的表,因为这个子查询在执行中,其实就相当于是个视图了。
后记
其实这个问题还可以衍生:
我猜想,像MYSQL那种外键可以连主键的前缀部分字段,应该无法支持这种特性,因为不是一对多,而是多对多,关联结果的行不能唯一对应外表的行。
也就是说,理论上,如果一个数据库设计成了外键必须连主键的所有字段,从原理上来看,它可以支持类似ORACLE的可更新的多表关联视图;相反,如果一个数据库要兼容MYSQL的外键设计,那么就可能需要舍弃掉可更新的多表关联视图的功能。
欢迎拍砖。