SQL 建表约束:PRIMARY KEY / FOREIGN KEY / CHECK 写法

建表时声明约束是数据库设计的基础,常见的有主键约束、外键约束和 CHECK 约束。 场景:供应商-零件多对多关系 经典的供应商(Supplier)-零件(Part)-供应(Supply)三表结构:供应商可以供应多种零件(M:N 关系) 每对供应商+零件组合有一个供应价格关系模式(3NF) Supplier(Sno PK, Sname, City) Part(Pno PK, Pname, Color, Weight) Supply(Sno PK FK, Pno PK FK, Price)Supply 的主键是复合主键 (Sno, Pno),同时 Sno 和 Pno 各自是外键。 SQL 建表 CREATE TABLE Supplier ( Sno CHAR(10) NOT NULL, Sname VARCHAR(50) NOT NULL, City VARCHAR(50), PRIMARY KEY (Sno) );CREATE TABLE Part ( Pno CHAR(10) NOT NULL, Pname VARCHAR(50) NOT NULL, Color VARCHAR(20), Weight DECIMAL(8,2), PRIMARY KEY (Pno) );CREATE TABLE Supply ( Sno CHAR(10) NOT NULL, Pno CHAR(10) NOT NULL, Price DECIMAL(10,2) NOT NULL, PRIMARY KEY (Sno, Pno), FOREIGN KEY (Sno) REFERENCES Supplier(Sno) ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (Pno) REFERENCES Part(Pno) ON DELETE CASCADE ON UPDATE CASCADE, CHECK (Price > 0) );各约束说明 PRIMARY KEY:不能为空,不能重复。复合主键用 PRIMARY KEY (col1, col2) 表格级声明,不能用列级。 FOREIGN KEY:引用另一张表的主键或唯一键,保证参照完整性。ON DELETE CASCADE 表示父表删除时子表自动级联删除。 CHECK:定义列值的范围或条件,CHECK (Price > 0) 要求价格必须大于零。 NOT NULL:禁止 NULL 值,通常和主键一起用。 常用查询 查询"供应红色零件的供应商名称": SELECT DISTINCT s.Sname FROM Supplier s JOIN Supply sp ON s.Sno = sp.Sno JOIN Part p ON sp.Pno = p.Pno WHERE p.Color = '红色';或用子查询: SELECT Sname FROM Supplier WHERE Sno IN ( SELECT sp.Sno FROM Supply sp JOIN Part p ON sp.Pno = p.Pno WHERE p.Color = '红色' );约束的列级 vs 表级写法 列级(约束跟在列后面,只作用于该列): CREATE TABLE t ( id INT PRIMARY KEY, val INT CHECK (val > 0), fk INT REFERENCES other(id) );表级(单独一行,支持多列主键和复合外键): CREATE TABLE t ( id1 INT, id2 INT, val INT, PRIMARY KEY (id1, id2), -- 复合主键必须用表级 FOREIGN KEY (id1) REFERENCES a(id), CHECK (val > 0) );复合主键(多列联合唯一)只能用表级声明;单列约束两种写法均可。

供应关系 E-R 建模 + 3NF + SQL:一道数据库题的标准答案

数据库课程有一道经典题目:供应商-零件-供应关系建模。是标准的 M:N 联系带属性,按 E-R → 3NF → SQL 完整走一遍。 题目 某企业采购管理系统:供应商 (Supplier):编号 Sno、名称 Sname、城市 City 零件 (Part):编号 Pno、名称 Pname、颜色 Color、重量 Weight 供应商可以供应多种零件,每种零件可由多个供应商供应 每个供应商对每种零件有一个供应价格 Price要求:画 E-R 图 设计满足 3NF 的关系模式 SQL 建"供应"表,带主键、外键、价格 > 0 约束 写"供应红色零件的供应商名称"查询第一步:E-R 图 两个实体、一个联系: ┌──────────────────┐ │ Supplier │ │──────────────────│ │ Sno (PK) │ │ Sname │ │ City │ └────────┬─────────┘ │ M │ ┌──────┴──────┐ │ Supply │◄── Price(联系属性) └──────┬──────┘ │ N │ ┌────────┴─────────┐ │ Part │ │──────────────────│ │ Pno (PK) │ │ Pname │ │ Color │ │ Weight │ └──────────────────┘关系类型:Supplier M——(Supply)——N Part,Supply 联系有属性 Price。 第二步:3NF 关系模式 M:N 联系必须独立成一张表,不能合并到任何一端。 Supplier 表: Supplier ( Sno ← 主键 (PK) Sname City )Part 表: Part ( Pno ← 主键 (PK) Pname Color Weight )Supply 表(M:N 联系变实体): Supply ( Sno ← 主键的一部分, 外键 → Supplier(Sno) Pno ← 主键的一部分, 外键 → Part(Pno) Price )Supply 的主键是 (Sno, Pno) 联合主键——同一个供应商同一个零件只能有一条供应记录,不同的价格意味着不同的关系需要建模成"报价历史"。 为什么满足 3NF:每个表所有非主属性都完全依赖于主键(无部分依赖 → 2NF) 每个表非主属性只依赖主键,不依赖其它非主属性(无传递依赖 → 3NF)第三步:SQL 建"供应"表 CREATE TABLE Supply ( Sno CHAR(10) NOT NULL, Pno CHAR(10) NOT NULL, Price DECIMAL(10,2) NOT NULL, PRIMARY KEY (Sno, Pno), FOREIGN KEY (Sno) REFERENCES Supplier(Sno), FOREIGN KEY (Pno) REFERENCES Part(Pno), CHECK (Price > 0) );要点:联合主键:PRIMARY KEY (Sno, Pno) 一起才唯一 外键:Sno 必须在 Supplier 里存在、Pno 必须在 Part 里存在 CHECK 约束:Price > 0 由数据库强制,即使应用忘了校验也保证不了负价 DECIMAL(10,2) 而不是 FLOAT——金额永远用定点数,浮点会有精度问题第四步:查询"供应红色零件的供应商名称" 需要连 Supplier / Supply / Part 三张表: SELECT DISTINCT S.Sname FROM Supplier S JOIN Supply SP ON S.Sno = SP.Sno JOIN Part P ON SP.Pno = P.Pno WHERE P.Color = '红色';关键点:DISTINCT — 一个供应商可能供应多种红色零件,去重 JOIN — 内连接,只要有匹配就出结果 WHERE P.Color = '红色' — 过滤条件不用 JOIN 也能写(子查询版本): SELECT DISTINCT Sname FROM Supplier WHERE Sno IN ( SELECT Sno FROM Supply WHERE Pno IN ( SELECT Pno FROM Part WHERE Color = '红色' ) );两种写法结果一样。现代 SQL 优化器基本能把子查询版本优化成 JOIN,但读起来 JOIN 更直观。 一些延伸 要报价历史怎么办? 加一个 EffectiveDate: CREATE TABLE SupplyHistory ( Sno CHAR(10), Pno CHAR(10), EffectiveDate DATE, Price DECIMAL(10, 2), PRIMARY KEY (Sno, Pno, EffectiveDate), FOREIGN KEY (Sno) REFERENCES Supplier(Sno), FOREIGN KEY (Pno) REFERENCES Part(Pno), CHECK (Price > 0) );查最新价格: SELECT Sno, Pno, Price FROM SupplyHistory sh WHERE EffectiveDate = ( SELECT MAX(EffectiveDate) FROM SupplyHistory WHERE Sno = sh.Sno AND Pno = sh.Pno );现代 SQL 也可以用窗口函数: SELECT Sno, Pno, Price FROM ( SELECT Sno, Pno, Price, ROW_NUMBER() OVER (PARTITION BY Sno, Pno ORDER BY EffectiveDate DESC) rn FROM SupplyHistory ) t WHERE rn = 1;一句话总结 M:N 联系带属性必须独立成表,联合主键 + 两个外键。3NF 的关系模式设计基本是"每个 M:N 拆一张表 + 每张表消除传递依赖"。查询用 JOIN 比多层子查询好读。

EIP-1559 Gas 费计算与 Python 转账清仓脚本

EIP-1559 的 Gas 费有三个参数,容易写错导致交易失败或余额卡住。 三个参数的含义参数 含义baseFee 当前区块的基础 Gas 价格,由网络自动调整,不能低于这个值maxPriorityFeePerGas 给矿工的小费(Tip),影响打包速度maxFeePerGas 用户愿意支付的最高单价,节点用 max(baseFee + tip, maxFeePerGas) 结算实际每笔交易扣费:gasUsed × (baseFee + tip),多余部分退还。 常见错误写法 # 问题:maxFee 等于 baseFee + priority,baseFee 稍微上涨就失效 max_fee_per_gas = base_fee + priority_fee如果在你签名到广播之间的时间里,下一个区块的 baseFee 上涨,节点会拒绝交易(因为 maxFeePerGas < baseFee)。 推荐写法 base_fee = web3.eth.get_block("pending")["baseFeePerGas"]try: priority_fee = web3.eth.max_priority_fee except Exception: priority_fee = web3.to_wei(0.1, "gwei")# baseFee 乘以 2 留有余量(MetaMask 等钱包的标准做法) max_fee_per_gas = base_fee * 2 + priority_fee清仓转账(全额转出) 把钱包余额全部转出时,amount = balance - fee 看起来简单,但有几个坑: from web3 import Web3def transfer_all(web3, private_key, dst_address): account = web3.eth.account.from_key(private_key) from_address = account.address balance = web3.eth.get_balance(from_address) nonce = web3.eth.get_transaction_count(from_address, "pending") base_fee = web3.eth.get_block("pending")["baseFeePerGas"] try: priority_fee = web3.eth.max_priority_fee except Exception: priority_fee = web3.to_wei(0.1, "gwei") max_fee_per_gas = base_fee * 2 + priority_fee # estimate_gas 比写死 30000 更准确 gas_limit = web3.eth.estimate_gas({ "from": from_address, "to": dst_address, "value": 1 }) gas_limit = int(gas_limit * 1.2) fee = gas_limit * max_fee_per_gas # 额外留安全余量,防止 baseFee 微小变化导致不足 reserve = web3.to_wei(0.00000001, "ether") amount = balance - fee - reserve if amount <= 0: print("余额不足支付 Gas 费") return None tx = { "from": from_address, "to": Web3.to_checksum_address(dst_address), "value": amount, "gas": gas_limit, "maxFeePerGas": max_fee_per_gas, "maxPriorityFeePerGas": priority_fee, "nonce": nonce, "chainId": web3.eth.chain_id, "type": 2, } signed = web3.eth.account.sign_transaction(tx, private_key) tx_hash = web3.eth.send_raw_transaction(signed.raw_transaction) print(f"发送地址: {from_address}") print(f"转出: {web3.from_wei(amount, 'ether')} ETH") print(f"Gas 费: {web3.from_wei(fee, 'ether')} ETH") print(f"TX: {tx_hash.hex()}") return tx_hash.hex()关键细节 nonce 用 "pending" 而非 "latest":如果有未确认交易,用 "latest" 得到的 nonce 偏小,新交易会被拒绝。 gas_limit 用 estimateGas:普通 ETH 转账是 21000,但某些链可能不同,estimate_gas * 1.2 更稳妥。 chainId 用 web3.eth.chain_id 获取:不要硬编码,避免接错链。 确认当前链 ID print(web3.eth.chain_id)Gravity L1 Mainnet 的 Chain ID 是 127001,支持 EIP-1559。 两步计算更可靠 对于高精度要求的清仓场景,建议:先构造 value=0 估算 gasLimit 计算 fee = gasLimit × maxFeePerGas,加安全余量 签名广播 如果仍然报 insufficient funds(baseFee 变化导致),重新读取参数再签一次

EIP-1559 Gas 费计算:Python Web3 转账扣余额的正确姿势

做链上转账时,想把账户余额全部转走,需要提前扣除 Gas 费。EIP-1559 下的费用计算和普通 Legacy 交易有些区别,容易算错。 常见的错误写法 base_fee = w3.eth.get_block("latest")["baseFeePerGas"] priority_fee = w3.to_wei(0.1, "gwei") max_fee_per_gas = base_fee + priority_feegas_limit = 30000 fee = gas_limit * max_fee_per_gasamount = balance - fee这里有两个问题: 问题一:fee 计算偏高但不算错 gas_limit * max_fee_per_gas 是最坏情况下的费用上限,实际扣款是: actual_fee = gas_used × (base_fee + priority_fee)gas_used ≤ gas_limit,所以实际花费通常比预留值少。转账类交易 gas_used 基本等于 gas_limit,误差不大;合约调用误差可能较大。 问题二:maxFeePerGas 应该 > base_fee + priority_fee EIP-1559 规范里,maxFeePerGas 是你愿意支付的上限,真正矿工收到的是: effective_gas_price = base_fee + priority_fee如果下一个区块 base_fee 涨了,用 maxFeePerGas = base_fee + priority_fee 可能导致交易因 maxFeePerGas < 新 base_fee 而被拒绝。 正确写法 def send_all_balance(w3, private_key, to_address, chain_id): account = w3.eth.account.from_key(private_key) from_address = account.address balance = w3.eth.get_balance(from_address) nonce = w3.eth.get_transaction_count(from_address) # base_fee 加 20% 缓冲,防止下一块涨价 base_fee = w3.eth.get_block("latest")["baseFeePerGas"] base_fee_with_buffer = int(base_fee * 1.2) priority_fee = w3.to_wei(0.1, "gwei") max_fee_per_gas = base_fee_with_buffer + priority_fee gas_limit = 21000 # 纯转账固定 21000;合约调用需要 estimateGas # 预留的 Gas 费上限 max_gas_cost = gas_limit * max_fee_per_gas amount = balance - max_gas_cost if amount <= 0: print(f"余额不足:balance={w3.from_wei(balance, 'ether')} ETH") return None tx = { "from": from_address, "to": w3.to_checksum_address(to_address), "value": amount, "gas": gas_limit, "maxFeePerGas": max_fee_per_gas, "maxPriorityFeePerGas": priority_fee, "nonce": nonce, "chainId": chain_id, "type": 2, } signed = w3.eth.account.sign_transaction(tx, private_key) tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction) print(f"发送: {w3.from_wei(amount, 'ether')} ETH") print(f"Gas 预留: {w3.from_wei(max_gas_cost, 'ether')} ETH") print(f"TX: {tx_hash.hex()}") return tx_hash.hex()EIP-1559 费用结构字段 含义baseFeePerGas 协议自动设置,每笔交易必须支付,销毁maxPriorityFeePerGas 给矿工的小费,激励打包maxFeePerGas 用户愿意支付的上限实际每 Gas 费用 min(maxFeePerGas, baseFee + priorityFee)实际从账户扣除: actual_cost = gas_used × effective_gas_price其中 effective_gas_price = baseFee + priorityFee(不超过 maxFeePerGas)。 多余的 (maxFeePerGas - effectiveGasPrice) × gasUsed 会退还给发送方,但退还发生在交易上链后,所以发送前账户余额必须 ≥ gasLimit × maxFeePerGas。 纯转账 vs 合约调用 纯 ETH 转账 gas 固定是 21000,可以直接写死。合约调用要用 estimate_gas: gas_limit = w3.eth.estimate_gas({ "from": from_address, "to": contract_address, "data": encoded_data, }) gas_limit = int(gas_limit * 1.1) # 加 10% 缓冲合约执行失败时 gas 仍会被扣,所以建议先 call 验证不会 revert 再发送。 常见的链 gas_limit 不同链的标准转账 gas 不完全一样,普通 EVM 链一般是 21000,部分 L2 有所不同: GAS_LIMITS = { "ethereum": 21000, "polygon": 21000, "bsc": 21000, "gravity": 30000, # 某些链会高一些 }不确定的情况下用 estimate_gas 最安全。 一句话总结 EIP-1559 转账预留费用用 gasLimit × maxFeePerGas,maxFeePerGas 要比当前 baseFee 高出一定缓冲(一般 20%),防止下一块 baseFee 上涨导致交易失败。纯转账 gas 固定 21000,合约调用用 estimateGas。

EIP-1559 交易脚本的六个 Gas 坑:baseFee 突变到 nonce 冲突

写一个"清仓转账"的脚本——把地址里所有余额扫到目标地址,用 EIP-1559。核心代码大致这样: base_fee = w3.eth.get_block("latest")["baseFeePerGas"] priority_fee = w3.to_wei(0.1, "gwei") max_fee_per_gas = base_fee + priority_feegas_limit = 30000 fee = gas_limit * max_fee_per_gas amount = balance - feetx = { "to": w3.to_checksum_address(dst), "value": amount, "gas": gas_limit, "maxFeePerGas": max_fee_per_gas, "maxPriorityFeePerGas": priority_fee, "nonce": w3.eth.get_transaction_count(from_address), "chainId": CHAIN_ID, "type": 2, }跑起来经常报: insufficient funds for gas * price + value问题不是一处,是好几个细节叠加。 坑 1:maxFeePerGas 写死等于 baseFee + priority 节点校验时: balance >= value + gasLimit × maxFeePerGasbaseFee 每个区块都会浮动(EIP-1559 每次可变 ±12.5%)。你算完刚好够,几秒后新块 baseFee 涨了: max_fee < new_base_fee + priority要么交易过不了、要么"钱不够"。MetaMask 的做法是: max_fee_per_gas = base_fee * 2 + priority_feebase_fee * 2 是常规冗余系数——baseFee 一个区块最多涨 12.5%,2x 完全够缓冲。多花的部分节点会退还给你,只按 effectiveGasPrice 收。 坑 2:gasLimit 硬编码 普通转账 21000 够,Gravity 这类链要 30000。合约调用差别就更大了。别写死,先估算再加冗余: gas_limit = w3.eth.estimate_gas({ "from": from_address, "to": dst, "value": 1, }) gas_limit = int(gas_limit * 1.2)* 1.2 是通用的 20% 余量,避免 estimation 边缘 case。 坑 3:priority fee 硬编码 0.1 gwei 网络拥堵时 0.1 gwei 的小费根本进不了下一个块。用节点建议值: priority_fee = w3.eth.max_priority_fee或者查 feeHistory 取近期百分位: history = w3.eth.fee_history(5, "latest", [50]) priority_fee = max(history["reward"][-1][0], w3.to_wei(1, "gwei"))坑 4:nonce 用了 latest 默认: w3.eth.get_transaction_count(from_address) # 等价于 "latest"如果地址有未确认的 pending 交易,latest 会返回过时的 nonce,新交易和旧的撞车,两个都卡住。改成: nonce = w3.eth.get_transaction_count(from_address, "pending")坑 5:清仓转账没留冗余 amount = balance - fee 是理论极限。实际因为 baseFee 突变或 estimation 偏差,几乎必然凑不够。留一点小额缓冲: reserve = w3.to_wei(0.0001, "ether") amount = balance - fee - reserve坑 6:chainId 抄错 "chainId": 127001,链 ID 不匹配的交易节点直接拒。跟节点确认一次: print(w3.eth.chain_id)修正后的样板 base_fee = w3.eth.get_block("latest")["baseFeePerGas"] priority_fee = w3.eth.max_priority_fee max_fee_per_gas = base_fee * 2 + priority_feegas_limit = w3.eth.estimate_gas({ "from": from_address, "to": dst, "value": 1, }) gas_limit = int(gas_limit * 1.2)fee_ceiling = gas_limit * max_fee_per_gas reserve = w3.to_wei(0.0001, "ether") amount = balance - fee_ceiling - reserve if amount <= 0: raise RuntimeError("余额不足以支付 Gas + 冗余")tx = { "to": w3.to_checksum_address(dst), "value": amount, "gas": gas_limit, "maxFeePerGas": max_fee_per_gas, "maxPriorityFeePerGas": priority_fee, "nonce": w3.eth.get_transaction_count(from_address, "pending"), "chainId": w3.eth.chain_id, "type": 2, }一句话总结 EIP-1559 参数不是"算准就行",是"抗突变才稳"。maxFee = baseFee * 2 + priority、gas = estimate * 1.2、nonce = pending,三件事一起做,出问题的概率会低一个数量级。