AI编程(五十二):更新和删除 — UPDATE+DELETE,让数据改得了、删得掉

作者:

在

上一篇让数据库学会了”答题”,今天给它配齐另一半本事:改错(UPDATE)和清理(DELETE)。价格录错了怎么改、测试垃圾怎么清,听起来都是小事——但一条没带 WHERE 的 DELETE,能让整张表瞬间清空。今天既学这两句 SQL,更学”给猛兽拴绳子”的安全流程。

一、真实场景:录错的 2990,和越堆越多的垃圾数据

接着价格监控工具的故事讲。数据库上线一个月,新问题来了,而且都是”改”出来的麻烦:

  1. 录错了:10月25日查机械键盘,299 块的价,手滑多打一个 0,录成了 2990。2000 条记录里混着一条错数据,报表全被它搅黄——怎么改?
  2. 堆垃圾:每次调试脚本都往表里塞几条测试数据,”测试商品””test123″越堆越多,查询结果里全是噪音——怎么清?

放在一个月前,这俩活儿是这样的:改 CSV 用 Excel 打开,小心翼翼改那一格,顺手把整列格式吃掉;删数据是”整行选中、右键删除、祈祷没删错行”。而数据库世界里,这两件事各只要一句 SQL:

  • UPDATE:指名道姓地”把哪行的哪个值,改成什么”;
  • DELETE:指名道姓地”把哪些行,请出去”。

注意我反复说”指名道姓”。这两句 SQL 和上一篇的 SELECT 共享同一个 WHERE——你会挑行,就等于会改行、删行。但反过来,它们也共享 SELECT 没有的风险:SELECT 写错条件,大不了查出来的东西不对;UPDATE、DELETE 写错条件(或者干脆忘了写),数据当场物理消失,没有 Ctrl+Z。所以今天的真正主菜不是语法,是”给猛兽拴绳子”的安全流程。先打个预防针,后面细说。

二、先备好料:还是那 11 条老朋友(外加一条”故意录错”的)

和上一篇一样的练习场,只有一个小改动:10月25日那条机械键盘,故意录成 2990,留给今天当”手术对象”:

# setup.py — 备好练习用的价格数据(含一条故意录错的)
import sqlite3

conn = sqlite3.connect("prices.db")
cur = conn.cursor()

cur.execute("""
CREATE TABLE IF NOT EXISTS prices (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    product    TEXT    NOT NULL,
    price      REAL    NOT NULL,
    checked_at TEXT    NOT NULL
)
""")

cur.execute("DELETE FROM prices WHERE id > 0")

rows = [
    ("机械键盘", 399.0, "2026-10-18 09:00"),
    ("机械键盘", 359.0, "2026-10-20 09:00"),
    ("机械键盘", 2990.0, "2026-10-25 09:00"),   # 故意录错!299 手滑成了 2990
    ("无线鼠标", 129.0, "2026-10-18 09:00"),
    ("无线鼠标",  99.0, "2026-10-22 09:00"),
    ("显示器",  1299.0, "2026-10-18 09:00"),
    ("显示器",  1199.0, "2026-10-23 09:00"),
    ("显示器",   899.0, "2026-10-27 09:00"),
    ("耳机",     199.0, "2026-10-19 09:00"),
    ("耳机",     199.0, "2026-10-26 09:00"),
    ("键盘膜",    29.9, "2026-10-21 09:00"),
]
cur.executemany("INSERT INTO prices (product, price, checked_at) VALUES (?, ?, ?)", rows)
conn.commit()
conn.close()
print("练习数据就位:11条价格记录(其中1条价格故意录错)")

跑完,11 条记录就位。注意最后那句 conn.commit()——上一篇它只是”顺手写着”,今天它会变成主角之一,坑点环节揭晓。

三、UPDATE:给数据动”微创手术”

语法就一句:

UPDATE 表名 SET 列 = 新值 WHERE 条件

翻译成人话:”在某张表里,找到满足条件的那些行,把指定的列改成新值。”上一篇学的 WHERE 直接复用。

手术1:把录错的 2990 改回 299。关键问题:WHERE 怎么写?”10月25日的机械键盘”——两个条件钉死一行:

# fix_price.py — 修正录错的价格(三步走:预览 → 执行 → 核对)
import sqlite3

conn = sqlite3.connect("prices.db")
cur = conn.cursor()

# 第1步:预览——先看看这句 WHERE 会碰到哪几行
cur.execute("""
    SELECT id, product, price, checked_at FROM prices
    WHERE product = '机械键盘' AND checked_at = '2026-10-25 09:00'
""")
print("将被修改的行:", cur.fetchall())
# 将被修改的行:[(3, '机械键盘', 2990.0, '2026-10-25 09:00')]  ← 只有一行,符合预期

# 第2步:执行
cur.execute("""
    UPDATE prices SET price = 299.0
    WHERE product = '机械键盘' AND checked_at = '2026-10-25 09:00'
""")
print("实际修改行数:", cur.rowcount)   # cur.rowcount:这次动作碰到几行
conn.commit()                          # 动数据必须提交!

# 第3步:核对——再查一遍,眼见为实
cur.execute("SELECT id, product, price FROM prices WHERE product = '机械键盘'")
print("修正后的机械键盘:", cur.fetchall())
conn.close()

输出:

将被修改的行:[(3, '机械键盘', 2990.0, '2026-10-25 09:00')]
实际修改行数: 1
修正后的机械键盘:[(1, '机械键盘', 399.0), (2, '机械键盘', 359.0), (3, '机械键盘', 299.0)]

盯一眼这个”三步走”:预览(SELECT 看会动哪几行)→ 执行(UPDATE)→ 核对(rowcount + 再查一遍)。它是今天真正的内功,任何改、删操作都套这个模板,后面不再重复。

顺便看两个新面孔:cur.rowcount 报告”这次操作实际碰到了几行”——预览说 1 行、rowcount 也报 1,两边对上才放心;conn.commit() 是”保存键”,第三节的坑点环节重点审判它。

手术2:一次改多列。SET 后面可以列一串,逗号隔开。比如发现这条记录连时间也录错了,一起改:

cur.execute("""
    UPDATE prices SET price = 299.0, checked_at = '2026-10-25 09:30'
    WHERE id = 3
""")
conn.commit()

注意这里 WHERE 换成了 id = 3。主键 id 是最稳的”指名道姓”——商品名可能重复、时间可能录错,但 id 独一无二。能用 id 定位就用 id。

手术3:批量改,让列自己参与计算。双十一全场九折,把 10月25日(含)之后的记录都打个折:

cur.execute("""
    UPDATE prices SET price = price * 0.9
    WHERE checked_at >= '2026-10-25'
""")
print("打折行数:", cur.rowcount)   # 打折行数: 3
conn.commit()

SET price = price * 0.9 里,右边的 price 是”改之前的旧值”——先读旧值、乘 0.9、写回新值,一步完成。这也叫”自增式更新”,将来 SET count = count + 1 这种计数器写法就是同一个套路。改完可以用 SELECT 验证:299 变 269.1、199 变 179.1、899 变 809.1。

四、DELETE:清理,但要”指名道姓”

语法同样朴素:

DELETE FROM 表名 WHERE 条件

清理1:删掉测试垃圾数据。假设表里混进了”测试商品”:

cur.execute("DELETE FROM prices WHERE product = '测试商品'")
print("删除行数:", cur.rowcount)
conn.commit()

清理2:删掉某个商品的全部历史。耳机下架了,历史不要了:

cur.execute("DELETE FROM prices WHERE product = '耳机'")
print("删除行数:", cur.rowcount)   # 2
conn.commit()

清理3:清空整张表——也要写明”删全部”。看回 setup.py 里的那句:

cur.execute("DELETE FROM prices WHERE id > 0")

“删 id 大于 0 的全部”,效果就是清空整张表(所有行的 id 都大于 0)。那为什么不直接 DELETE FROM prices 呢?因为裸奔的 DELETE 是出了名的危险分子——你分不清一行代码是”删全部”还是”忘了写 WHERE”,代码审查时一眼扫过去全是警报。把”删全部”写成 WHERE id > 0,意图白纸黑字,安全检查也认。这个习惯第 51 篇埋过伏笔,今天正式立为规矩。

DELETE 和 DROP TABLE 别搞混。DELETE 是”把抽屉里的纸全扔了,抽屉还在”——表结构还在,还能继续插新数据;DROP TABLE 是”连抽屉一起拆了”——表从此不存在,再查询直接报 no such table。一个是清空,一个是拆家。今天只学”扔纸”,”拆抽屉”的 DROP TABLE 等真有需要再说。

五、实战:给价格监控工具加一个”数据维护模式”

把三步走打包成函数,将来工具里哪天要改数据、删数据,直接调用:

# maintain.py — 数据维护工具箱
import sqlite3

def fix_price(product, checked_at, new_price):
    """修正某条记录的价格:预览 → 执行 → 核对"""
    conn = sqlite3.connect("prices.db")
    cur = conn.cursor()

    # 预览
    cur.execute(
        "SELECT id, price FROM prices WHERE product = ? AND checked_at = ?",
        (product, checked_at),
    )
    target = cur.fetchall()
    print(f"将被修改:{target}")
    if len(target) != 1:
        print(f"⚠ 条件命中 {len(target)} 行(预期1行),中止操作")
        conn.close()
        return False

    # 执行
    cur.execute(
        "UPDATE prices SET price = ? WHERE product = ? AND checked_at = ?",
        (new_price, product, checked_at),
    )
    touched = cur.rowcount
    conn.commit()
    conn.close()

    # 核对
    print(f"实际修改 {touched} 行")
    return touched == 1


def purge_product(product):
    """清掉某商品的全部历史:预览 → 执行 → 核对"""
    conn = sqlite3.connect("prices.db")
    cur = conn.cursor()

    cur.execute("SELECT COUNT(*) FROM prices WHERE product = ?", (product,))
    n = cur.fetchone()[0]
    print(f"'{product}' 共有 {n} 条记录待删")

    cur.execute("DELETE FROM prices WHERE product = ?", (product,))
    touched = cur.rowcount
    conn.commit()
    conn.close()

    print(f"实际删除 {touched} 行")
    return touched == n


# 跑一遍:修正录错的价格、清掉键盘膜
print(fix_price("机械键盘", "2026-10-25 09:00", 299.0))
print(purge_product("键盘膜"))

输出:

将被修改:[(3, 2990.0)]
实际修改 1 行
True
'键盘膜' 共有 1 条记录待删
实际删除 1 行
True

三个设计细节值得抄走:

  1. 条件命中行数和预期不符就中止。fix_price 要求精确命中 1 行,多一行少一行都不动手——宁可不改,不可错改;
  2. 用 ? 占位符传值,和第 51 篇防注入是同一招,顺手治”加不加引号”的选择困难症;
  3. 函数返回 True/False,调用方知道这刀到底动没动。

这就是”跟 AI 说需求→读懂→改进”里”读懂”的含金量:AI 给你生成十行代码不难,难的是你看出”这里缺 rowcount 核对””这里没兜住多行命中”。今天之后,你看 UPDATE/DELETE 的代码,先找 WHERE,再找 commit,最后找核对——三样齐全才算合格。

六、三个必踩的坑(先打疫苗)

坑1:忘写 WHERE,一张表瞬间清空。这是数据库世界的”煤气泄漏”,必须单独强调。UPDATE prices SET price = 0 会把所有行的价格改成 0;DELETE FROM prices 会把所有行直接蒸发。而且这种事故往往发生在”我就想改一条””条件待会儿再加”的一念之间。疫苗三件套:

  1. WHERE 不是可选项——动数据的 SQL 不带 WHERE,不许进代码;
  2. 三步走——先 SELECT 预览”会动哪几行”,确认是预期的那几行,再动手;
  3. 大动作前备份——真要清历史数据,动手前给数据库文件拍个快照,一行代码的事:
import shutil
shutil.copy("prices.db", "prices.db.bak")   # 后悔药,成本几乎为零

删错了数据库没有撤销键,但这颗后悔药能救你的命。

坑2:改完忘了 commit(),白忙活一场。这是 Python 操作 SQLite 的头号暗坑:sqlite3 模块默认把你的改动攒着(自动开事务),不调用 conn.commit() 就 close(),所有 UPDATE/DELETE 全部作废。最迷惑的是现场表现:脚本明明打印”修改 1 行”,下次运行再查——数据又回去了,仿佛闹鬼。SELECT 不受影响(查询不用提交),所以前两篇你根本没机会踩到它,今天第一次动数据就撞上了。疫苗一句话:动数据的代码,commit() 永远写在 close() 的前一行,肌肉记忆。顺带说一句进阶的:with sqlite3.connect("prices.db") as conn: 这种写法会在正常退出时自动帮你提交,异常退出时自动回滚(撤销未提交的改动)——”回滚”就是事务的另一半后悔药,感受一下即可。

坑3:删错了没有 Ctrl+Z——所以真删之前,先问三个问题。DELETE 是物理删除,删完就是删完了。动手前默念三问:① WHERE 条件用 SELECT 跑过一遍了吗?② 预期影响行数是几,rowcount 对得上吗?③ 备份了吗?三问都点头,再按回车。如果数据珍贵(比如用户数据),行业里还有个更软的玩法叫软删除:表里加一列 status,”删”的时候不真删,改成 UPDATE ... SET status = 'deleted',查询时过滤掉这些行就行。数据还在,随时能”复活”——代价是每次查询都得多一个条件。删测试垃圾用真删,删正经数据考虑软删除,这个判断留给未来的你。

七、跟AI协作的正确姿势(改删特供版)

  1. 让 AI 出”三步走”,不出裸奔 SQL。“prices 表有 id、product、price、checked_at 四列。帮我把 10月25日机械键盘的价格从 2990 改成 299。要求:先给预览 SELECT,再给 UPDATE,再给核对查询,每句带注释。”——你验收的重点从”语法对不对”升级为”流程全不全”。
  2. 让 AI 先答”这句会动几行”。收到 UPDATE/DELETE 后追问:”这句在 11 条样例数据上执行,会命中几行?逐行说是哪几行。”AI 算出来的和你 SELECT 预览的对不上,就说明哪里没钉死。
  3. 批量删除让 AI 出备份方案。“帮我写清空 10月19日前历史数据的脚本”——合格的 AI 答案应该包含备份步骤和 rowcount 核对;如果它给你的是一行裸 DELETE,把上一段的要求补给它:”加预览、加备份、加核对。”
  4. 出错回炉带”三件套”。“改了但数据没变”(多半是坑2)、”删多了”(多半是坑1)这类故障,把 SQL 全文 + 你预期影响几行 + rowcount 实际报几行 贴给 AI,一次定位。

八、今日小练习

练习1:纠错 + 清理三连。用练习场的 11 条数据,每步都走三步走(预览→执行→核对):①把 10月25日机械键盘的 2990 改回 299;②删除”键盘膜”的全部记录;③把 10月25日(含)之后所有记录的价格打九折(price = price * 0.9),并核对打折后机械键盘是 269.1。

提示:①用商品名 + 时间两个条件钉死一行;③批量改之前,先 SELECT 数一数”10月25日之后”到底有几条,rowcount 要和它对上。

练习2:给你的价格监控脚本加”维护模式”。写两个函数:fix_price(product, checked_at, new_price) 修正单条记录、purge_product(product) 清掉某商品历史。要求和文章的 maintain.py 一样:命中行数不符预期就中止、用 ? 占位符、返回 True/False。

提示:fix_price 里那个 len(target) != 1 的保险丝是灵魂——想想如果没有它,商品名打错了会发生什么(0 行白跑,或者更糟:同名记录一起被改)。

练习3(坑点自考):先诊断,再对答案。下面每个操作都有毛病,不看文章说出病在哪、怎么改:①cur.execute("DELETE FROM prices")——本意是”删掉测试商品的记录”;②cur.execute("UPDATE prices SET price = 0 WHERE product = '显示器'") 之后直接 conn.close(),没别的代码;③cur.execute("UPDATE prices SET price = 99 WHERE price = NULL")——本意是”把没录价格的记录补成 99″。

提示:一句对一个坑(忘 WHERE、忘 commit、NULL 比较);②的”症状”描述想一想——什么现象会让你以为”闹鬼”了?

下篇预告

下一篇(第53篇):排序和分页 — ORDER BY+LIMIT,让答案排好队、分页端上来。查询结果乱序看着累,”最便宜的 3 个商品”“销量前十”这类问题更是天天有。ORDER BY 负责排队(谁前谁后),LIMIT 负责限量(只要前几名),俩人一搭档,排行榜、分页加载全搞定。上一篇练习里”每个商品只看最新一条”的高效写法,也在第 53 篇揭晓。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注