供应关系 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,三件事一起做,出问题的概率会低一个数量级。
Web3.py EIP-1559 交易 Gas 费计算:baseFee 波动与余额预留
EIP-1559 Gas 费结构 EIP-1559 交易有两个关键字段:maxFeePerGas:你愿意支付的最高单价(base fee + priority fee) maxPriorityFeePerGas:给矿工的小费("tip")节点在验证交易时检查: balance >= value + gasLimit × maxFeePerGas实际扣费是: gasUsed × (baseFeePerGas + priorityFeePerGas)其中 baseFeePerGas 由网络自动调整,gasUsed <= gasLimit。 典型实现(web3.py) from web3 import Web3w3 = Web3(Web3.HTTPProvider("https://mainnet-rpc.example.xyz"))def send_max(private_key, dst_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) # 动态读取 baseFee base_fee = w3.eth.get_block("latest")["baseFeePerGas"] # 用节点推荐的 priority fee,更可靠 priority_fee = w3.eth.max_priority_fee # maxFee 加 buffer,防 baseFee 上涨 max_fee_per_gas = base_fee * 2 + priority_fee # 估算 gas,比写死更安全 gas_limit = w3.eth.estimate_gas({ "from": from_address, "to": dst_address, "value": 1, }) gas_limit = int(gas_limit * 1.2) # 20% buffer fee = gas_limit * max_fee_per_gas # 预留少量 wei 防止因 baseFee 微小波动导致余额不足 reserve = w3.to_wei(0.000001, "ether") amount = balance - fee - reserve if amount <= 0: print(f"余额不足,balance={balance}, fee={fee}") 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": chain_id, "type": 2, } signed = w3.eth.account.sign_transaction(tx, private_key) tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction) return tx_hash.hex()常见坑 坑一:maxFee 太紧,baseFee 上涨就失败 # 错误写法:maxFee 刚好等于 baseFee + priority max_fee = base_fee + priority_fee # 一旦下一个区块 baseFee 上涨,交易立即失效改为: max_fee = base_fee * 2 + priority_fee # 留足余量,只多花极少的钱坑二:写死 gasLimit gas_limit = 21000 # ETH 原生转账是这个值,但其他链可能不同更安全的方式: gas_limit = w3.eth.estimate_gas({"from": from_addr, "to": dst, "value": 1}) gas_limit = int(gas_limit * 1.2)坑三:不预留 reserve 导致余额精确到 wei 后失败 节点验证 balance >= value + gasLimit × maxFeePerGas。如果 amount = balance - fee 算完后 baseFee 微涨,条件就不满足。 预留 0.000001 ETH(约 1,000,000,000,000 wei)几乎没有损失但能避免大量边缘失败。 坑四:priority_fee 写死 priority_fee = w3.to_wei(0.1, "gwei") # 可能太低导致交易迟迟不确认改为从节点获取推荐值: priority_fee = w3.eth.max_priority_fee # 节点返回当前合理的 tip
