课程  因子投资  机器学习  Python  Poetry  ppw  tools  programming  Numpy  Pandas  pandas  算法  hdbscan  聚类  选股  Algo  minimum  numpy  algo  FFT  模式识别  配对交易  GBDT  LightGBM  XGBoost  statistics  CDF  KS-Test  monte-carlo  VaR  回测  过拟合  algorithms  machine learning  strategy  python  sklearn  pdf  概率  数学  面试题  量化交易  策略分类  风险管理  Info  interview  career  xgboost  PCA  wavelet  时序事件归因  SHAP  Figures  Behavioral Economics  graduate  arma  garch  人物  职场  Quantopian  figure  Banz  金融行业  买方  卖方  story  量化传奇  rsi  zigzag  穹顶压力  因子  ESG  因子策略  投资  策略  pe  ORB  Xgboost  Alligator  Indicator  factor  alpha101  alpha  技术指标  wave  quant  algorithm  pearson  spearman  tushare  因子分析  Alphalens  涨停板  herd-behaviour  momentum  因子评估  review  SMC  聪明钱  trade  history  indicators  zscore  波动率  强化学习  顶背离  freshman  resources  others  AI  DeepSeek  network  量子计算  金融交易  IBM  weekly  LLT  backtest  backtrader  研报  papers  UBL  quantlib  jupyter-notebook  scikit-learn  pypinyin  qmt  xtquant  blog  static-site  duckdb  工具  colors  free resources  barra  world quant  Alpha  openbb  数据  risk-management  llm  prompt  CANSLIM  Augment  arsenal  copilot  vscode  code  量化数据存储  hdf5  h5py  cursor  augment  trae  Jupyter  jupysql  pyarrow  parquet  数据源  quantstats  实盘  clickhouse  notebook  redis  remote-agent  AI-tools  Moonshot  回测,研报,tushare 

tools »

致命的 ID -- DuckDB 中的 Returning 子句之谜


Table of Content

Duckdb是一个年轻但非常有潜力的数据库。但它也有桀骜不驯的一面:在一个普通的update语句执行时,出现了罕见的违反外键约束的问题。最终,依靠Augment这个强大的AI工具,我们找到了根本原因,并且通过坚实的实验验证了结论。

『华生,你是否曾思考过,在数据库的深处,隐藏着多少不为人知的秘密?』福尔摩斯放下手中的烟斗,凝视着窗外伦敦的雾霭。

『福尔摩斯,我承认数据库对我而言如同迷宫。』我诚实地回答,我正在记录福尔摩斯最新的冒险。

『今天早晨,一位焦虑的开发者前来求助,他遇到了一个令人费解的谜题。』福尔摩斯从桌上拿起那张写满 SQL 代码的纸条,『他的程序在执行一条看似无害的 UPDATE 语句时,突然抛出了外键约束错误。』

这是建表语句,这里有两张表,resources 和 resource_whitelist。resource_whitelist 表有一个外键引用 resources 表的 id 字段。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
CREATE SEQUENCE if not exists seq_resource_id START WITH 1 INCREMENT BY 1;
CREATE TABLE if not exists resources (
    id INTEGER PRIMARY KEY DEFAULT nextval('seq_resource_id'),
    course VARCHAR NOT NULL,
    resource VARCHAR NOT NULL,
    seq INTEGER NOT NULL,
    title VARCHAR NOT NULL,
    UNIQUE (course, rel_path)
);

CREATE SEQUENCE if not exists seq_resource_whitelist_id START WITH 1 INCREMENT BY 1;
CREATE TABLE if not exists resource_whitelist (
    id INTEGER PRIMARY KEY DEFAULT nextval('seq_resource_whitelist_id'),
    resource_id INTEGER NOT NULL,
    course VARCHAR NOT NULL,
    timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (customer_id) REFERENCES customers(id),
    FOREIGN KEY (resource_id) REFERENCES resources(id),
    UNIQUE (customer_id, resource_id)
);

Info

这是修改语句:

1
2
3
4
5
UPDATE resources
        SET seq = ?, title = ?, description = ?, 
        publish_date = ?, price = ?
        WHERE id = ?
        RETURNING id

『但这有什么奇怪的呢?』我问道,『外键约束不就是为了防止数据不一致吗?』

『我最初也是这么看』。福尔摩斯轻叹一口气,继续说道,『作为一个阅案无数的高手,我几乎立刻就回答了他:这个错误是因为在更新资源时违反了外键约束。错误信息表明 resource_id: 994 仍然被其他表中的外键引用,也就是是 resource_whitelist 表。我甚至还给出了修改方案。』

『但是,这位开发者并没有满足于我的修改方案,而是对我的答案进行了质疑』。福尔摩斯说道。

『质疑我们的福尔摩斯!』我不由得提高了音量。

『不幸的是,我的朋友』,福尔摩斯皱了皱眉,『这位开发者的质疑是有道理的。我的确应该看到更仔细一些,查出背后真正的元凶,再下结论。你知道,巴斯克维尔的猎犬案之后,我一直有点没恢复过来』。

巴斯克维尔的猎犬案涉及到一个古老的家族诅咒,传说中有一只巨大的恶魔猎犬在巴斯克维尔家族的领地上出没,专门袭击家族成员。这种超自然的元素使得案件一开始就笼罩在神秘和恐怖的氛围中,让调查变得异常困难,并且一度影响到了福尔摩斯的声誉。对此我完全赞同。

『问题的关键在于,华生』,福尔摩斯轻敲桌面,『这位开发者并未尝试更改任何主键,也没有删除任何记录。他仅仅是更新了一些无关紧要的字段,比如标题或描述。』

『那么,为什么会触发外键约束错误呢?』

『正是这一点引起了我的兴趣!』福尔摩斯站起身来,开始在房间里踱步,『我们面对的是 DuckDB,一个年轻而有趣的数据库系统。错误信息中提到了'foreign key limitations',这暗示着某种不寻常的行为。』

『你有什么理论吗,福尔摩斯?』

『我设计了一个实验,华生。』福尔摩斯拿出一张写满代码的纸,三个简单的测试案例,足以揭示真相。』

我凑近看了看那些 Python 代码,『看起来很复杂。』

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import duckdb
import logging

# 设置日志
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

# 创建测试数据库
conn = duckdb.connect(':memory:')

# 创建测试表结构
conn.execute(『『『
CREATE TABLE parent (
    id INTEGER PRIMARY KEY,
    name VARCHAR
);

CREATE TABLE child (
    id INTEGER PRIMARY KEY,
    parent_id INTEGER,
    data VARCHAR,
    FOREIGN KEY (parent_id) REFERENCES parent(id)
);
『『『)

# 插入测试数据
conn.execute(INSERT INTO parent VALUES (1, 'Parent 1'), (2, 'Parent 2'))
conn.execute(INSERT INTO child VALUES (101, 1, 'Child 1'), (102, 2, 'Child 2'))

# 测试 1: 正常更新非键字段
try:
    logger.info(测试 1: 更新 parent 表的非键字段)
    conn.execute(UPDATE parent SET name = 'Updated Parent 1' WHERE id = 1)
    logger.info(测试 1 成功可以更新非键字段)
except Exception as e:
    logger.error(f测试 1 失败{e})

# 测试 2: 使用 RETURNING 子句更新
try:
    logger.info(测试 2: 使用 RETURNING 子句更新)
    result = conn.execute(UPDATE parent SET name = 'Updated Again' WHERE id = 1 RETURNING id).fetchall()
    logger.info(f测试 2 成功RETURNING 子句返回{result})
except Exception as e:
    logger.error(f测试 2 失败{e})

# 测试 3: 尝试更新被引用的主键
try:
    logger.info(测试 4: 尝试更新被引用的主键)
    conn.execute(UPDATE parent SET id = 3 WHERE id = 1)
    logger.info(测试 4 成功可以更新被引用的主键)
except Exception as e:
    logger.error(f测试 4 失败{e})
    if foreign key in str(e).lower():
        logger.info(确认更新被引用的主键会触发外键约束错误)

# 显示最终数据
parent_data = conn.execute(SELECT * FROM parent).fetchall()
child_data = conn.execute(SELECT * FROM child).fetchall()
logger.info(f最终 parent 表数据{parent_data})
logger.info(f最终 child 表数据{child_data})

『表面上看是如此。但真相往往隐藏在细节之中。』福尔摩斯微笑道,『第一个测试是普通的 UPDATE 操作,没有任何特殊子句。第二个测试添加了一个 RETURNING 子句。第三个测试则直接尝试更新被引用的主键。』

『结果如何?』

『啊,华生,结果令人着迷!『福尔摩斯的眼睛闪烁着兴奋的光芒,『第一个测试完美通过,证明普通的 UPDATE 操作可以正常工作。第三个测试如预期般失败,因为它确实违反了外键约束。』

『那么第二个测试呢?』

『第二个测试失败了,『福尔摩斯停顿了一下,『但第二个测试,华生,第二个测试揭示了真相!』

『怎么说?』

『带有 RETURNING 子句的 UPDATE 操作触发了外键约束错误,尽管它只是更新了非键字段!『福尔摩斯高声宣布,『这证明 DuckDB 在处理带 RETURNING 子句的 UPDATE 操作时,采用了不同的执行路径。它很可能在内部将 UPDATE 实现为'先 DELETE 再 INSERT'的组合操作!』

『太不可思议了,福尔摩斯!』

『初看之下,这似乎是个 bug。但从更深层次看,这是 DuckDB 实现细节的一个特性。『福尔摩斯重新坐下,『当使用 RETURNING 子句时,DuckDB 需要返回受影响行的信息。为了实现这一点,它可能选择了一种不同的执行策略,这种策略会触发完整的外键约束检查。』

『等一下!』我小声地喊起来,『这里还因为 resource 的主键是自增的!所以,当删除原记录,再新增记录时,尽管记录的语义没有改变,但它们的 id 字段却意外更新了』。

『你说得很对!』福尔摩斯微笑着说。

『那么解决方案是什么?』

『简单明了,华生。『福尔摩斯微笑着说,『要么避免在有外键引用的表上使用 RETURNING 子句,要么采用两步操作:先查询,再更新。』

『福尔摩斯,你总是能找到最简单的解决方案。』

『在数据库的世界里,华生,表面上看似简单的操作背后,往往隐藏着复杂的实现细节。』福尔摩斯拿起小提琴,拉出一段欢快的旋律,正如福尔摩斯常说:排除所有不可能的情况后,剩下的,无论多么不可思议,一定就是真相。

『那么这个案例可以称为什么呢?』我问道,准备为新的笔记命名。

『就叫它'RETURNING 子句之谜'吧,华生。』福尔摩斯微笑着回答,『一个小小的 SQL 子句,揭示了数据库引擎深处的秘密。』

窗外,伦敦的雾气渐渐散去,又一个数据库之谜被成功破解。