上一篇把2000条价格历史搬进了SQLite新家,今天开始享受”图书馆”的待遇:把”上周三到今天机械键盘降了几次价”这种人话,翻译成 SELECT … WHERE …,让数据库替你翻完2000条记录、只把答案端给你。查询是数据库的看家本领——从今天起,你问问题,程序答问题。
一、真实场景:2000条历史里,藏着你想知道的答案
接着价格监控工具的故事讲。第50篇你把 price_history.csv 里的2000多条记录搬进了 prices.db,房子建好了、东西归位了。现在老板(或者你自己)开始问问题了:
- “机械键盘现在什么价?”
- “哪些商品跌破过100块?”
- “10月20号那天,都查了哪些商品的价格?”
放在一个月前,这些问题意味着:打开CSV、拖动滚动条、肉眼扫描2000行,或者写一段Python循环逐行判断。而数据库的本职工作就是回答这种问题——你负责把问题说清楚(SELECT),它负责翻完整张表(哪怕20万条),把答案端上来。
今天的武器就两句SQL,但它们覆盖了你未来80%的查询需求:
- SELECT:说清楚”我要看哪些列”;
- WHERE:说清楚”我要哪些行”。
一个是”看哪几栏”,一个是”挑哪几条”。组合起来,就是”从2000条里精准捞出我要的那几条、只给我看我想看的那几栏”。
二、先备好料:3分钟重建练习场
为了所有人跑出来的结果一致,先用一段脚本把练习数据备好(如果你留着第50篇的 prices.db,直接用也行,但建议先跑这段,保证数据一模一样):
# 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
)
""")
# 清空旧数据,保证每次练习结果一致(学查询而已,别心疼)
# 小细节:DELETE 习惯性带上 WHERE——"删全部"也要写明"删 id 大于 0 的全部",
# 裸奔的 DELETE 是出了名的危险分子,第52篇专门审判它
cur.execute("DELETE FROM prices WHERE id > 0")
rows = [
("机械键盘", 399.0, "2026-10-18 09:00"),
("机械键盘", 359.0, "2026-10-20 09:00"),
("机械键盘", 299.0, "2026-10-25 09:00"),
("无线鼠标", 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条价格记录")
跑完,11条记录躺进了 prices.db。数量不多,但”商品、价格、时间”的结构和2000条的生产库一模一样——学查询用11条还是2000条,写法完全一样,这就是数据库的爽点。
三、SELECT:说清楚”我要看哪几栏”
先学最简单的”整张表看一眼”:
# look.py — 把表里的东西取出来看
import sqlite3
conn = sqlite3.connect("prices.db")
cur = conn.cursor()
cur.execute("SELECT * FROM prices")
rows = cur.fetchall() # fetchall:把结果一次性全拿回来
for row in rows:
print(row)
conn.close()
SELECT * FROM prices 翻译成人话就是:”从 prices 表里,取所有列的所有行”。跑完你会看到11行元组,跟第50篇搬完家验收时看到的一样。
但”所有列”往往不是你想要的。比如你只想看”商品名和价格”,不想看编号和时间:
cur.execute("SELECT product, price FROM prices")
print(cur.fetchall())
# [('机械键盘', 399.0), ('机械键盘', 359.0), ...]
SELECT product, price 就是”只要这两栏”。列的顺序还能随你排:SELECT price, product 就是价格在前。三个写法先记牢:
| 写法 | 意思 | 什么时候用 |
|---|
|——|——|———–|
SELECT * |
所有列 | 临时看一眼、探索数据 |
|---|---|---|
SELECT product, price |
指定的几列 | 正式查询首选:只拿需要的,快且清爽 |
SELECT DISTINCT product |
指定列去重后的 distinct 值 | “一共有哪些商品?” |
DISTINCT 顺手学了,它回答的是”有哪些不重复的取值”:
cur.execute("SELECT DISTINCT product FROM prices")
print(cur.fetchall())
# [('机械键盘',), ('无线鼠标',), ('显示器',), ('耳机',), ('键盘膜',)]
11条记录里商品名有重复,DISTINCT 一去重,5种商品一目了然。记住一个习惯:给人看的查询,尽量写清楚列名,别偷懒用 *——将来表加到20列,* 会把你淹掉。
取结果的两个动作也正式认识一下:fetchall() 一次拿全部(结果多时吃内存),fetchone() 每次拿一行(适合”我只要一条”的场合)。今天先用 fetchall() 够了。
四、WHERE:给查询装一个”条件筛子”
SELECT 管”看哪几栏”,WHERE 管”挑哪几行”。这是今天真正的主菜。
第1问:机械键盘现在什么价?——条件是”商品名等于机械键盘”:
cur.execute("SELECT product, price, checked_at FROM prices WHERE product = '机械键盘'")
for row in cur.fetchall():
print(row)
# ('机械键盘', 399.0, '2026-10-18 09:00')
# ('机械键盘', 359.0, '2026-10-20 09:00')
# ('机械键盘', 299.0, '2026-10-25 09:00')
WHERE product = '机械键盘' 就是”只留商品名等于’机械键盘’的行”。注意两个细节:文本值要带引号(’机械键盘’),而等号在SQL里就是”等于”(不是 ==,这是新手第一个手滑点)。
第2问:哪些商品跌破过100块?——条件是”价格小于100″:
cur.execute("SELECT product, price FROM prices WHERE price < 100")
print(cur.fetchall())
# [('无线鼠标', 99.0), ('键盘膜', 29.9)]
数字比较不需要引号。常用的比较符跟数学里几乎一样:=(等于)、 或 !=(不等于)、>、<、>=、<=。再配一个”区间”专用写法:
# 价格在100到400之间的记录(含两端)
cur.execute("SELECT product, price FROM prices WHERE price BETWEEN 100 AND 400")
第3问:10月20号到25号之间,都查了哪些商品?——时间现在存的是 2026-10-20 09:00 这种统一格式的文本,文本比较大小,比的就是字典序。惊喜来了:这种格式的字典序恰好等于时间先后序,所以直接比就行:
cur.execute("""
SELECT product, price, checked_at FROM prices
WHERE checked_at >= '2026-10-20' AND checked_at < '2026-10-26'
""")
for row in cur.fetchall():
print(row)
注意这里来了第二个新零件:AND——两个条件”同时成立”才留下。它的兄弟 OR 是”任一成立就留下”:
# 便宜货(低于100)或者大件(超过1000)
cur.execute("SELECT product, price FROM prices WHERE price 1000")
print(cur.fetchall())
# [('无线鼠标', 99.0), ('显示器', 1299.0), ('显示器', 1199.0), ('键盘膜', 29.9)]
第4问:商品名”像”键盘的有哪些?——模糊匹配用 LIKE:
cur.execute("SELECT product, price FROM prices WHERE product LIKE '%键盘%'")
# % 是"任意一串字符"的通配符:%键盘% = 名字里含"键盘"
# '键盘%' = 以"键盘"开头;'键盘' = 必须严格等于
五大件齐活了,摆个全家福:
| 写法 | 人话 | 例句 |
|---|
|——|——|——|
WHERE price < 100 |
数值比较 | 跌破100的 |
|---|---|---|
WHERE product = '耳机' |
文本精确等于 | 就要耳机 |
WHERE price BETWEEN 100 AND 400 |
区间(含两端) | 100~400的 |
WHERE product LIKE '%键盘%' |
模糊匹配 | 名字带”键盘”的 |
AND / OR |
并且 / 或者 | 组合条件 |
五、组合技:把”人话问题”翻译成SQL
零件认全了,来解一道完整的应用题:“机械键盘一共降了几次价?每次什么价?”
先别急着写代码——本系列的老规矩,先把人话拆成”我要哪些行 + 我要哪些栏”:
- 行(WHERE):商品是机械键盘的记录;
- 栏(SELECT):价格、检查时间(按什么顺序看先不管,第52篇的排序正餐伺候)。
翻译:
# answer.py — 回答一个真实的业务问题
import sqlite3
conn = sqlite3.connect("prices.db")
cur = conn.cursor()
cur.execute("""
SELECT price, checked_at
FROM prices
WHERE product = '机械键盘'
""")
rows = cur.fetchall()
print(f"机械键盘共有 {len(rows)} 条价格记录:")
first_price = rows[0][0]
for price, checked_at in rows:
change = "(首次记录)" if price == first_price else (
"↑ 涨了" if price > first_price else "↓ 降了")
print(f"{checked_at} {price} 元 {change}")
conn.close()
输出:
机械键盘共有 3 条价格记录:
2026-10-18 09:00 399.0 元 (首次记录)
2026-10-20 09:00 359.0 元 ↓ 降了
2026-10-25 09:00 299.0 元 ↓ 降了
看清楚这个流程的分工:SQL负责”从2000条里捞出目标行”,Python负责”把捞出来的行加工成人话”。这个分工是后面几十篇所有实战的地基——数据库当”超级快的筛选器”,Python当”会算账、会讲人话的秘书”。
再把问题升级一档:”查一查有没有商品两次检查价格完全没变“(比如耳机199一直是199)。人话拆解:同一商品的两条记录价格相同。这得让表”自己跟自己比”,是后面第53篇多表/自关联的邻居——今天先老实说:这题超纲了。把问题记在小本本上,学完第52、53篇回头再看,会心一笑。学编程和学语文一样,知道”这题现在不会、该哪天来解”,本身就是一种会。
六、三个必踩的坑(先打疫苗)
坑1:文本值忘了引号,或者把数字加了引号。WHERE product = 机械键盘 会直接报 no such column: 机械键盘——数据库把没引号的”机械键盘”当成了列名。反过来 WHERE price = '99' 虽然多数时候”碰巧能跑”(SQLite会自动转换),但这是在赌数据库的宽容度,而且字符串 '99' > '100' 的比较结果跟你脑子里的数字比较完全两码事。疫苗:文本加引号、数字不加,记不清就统一用 ? 占位符让数据库操心:
cur.execute("SELECT * FROM prices WHERE product = ?", ("机械键盘",))
上一篇防注入的老朋友在这里照样好使,而且彻底治”加不加引号”的选择困难症。
坑2:AND 和 OR 混用,忘了加括号。来道题:查”显示器里低于100或者高于1000的极端价格记录”。随手写出来的是:
# 反面教材:899的显示器没进来,倒是混进了两个不速之客
cur.execute("SELECT product, price FROM prices WHERE price 1000 AND product = '显示器'")
print(cur.fetchall())
# [('无线鼠标', 99.0), ('显示器', 1299.0), ('显示器', 1199.0), ('键盘膜', 29.9)]
说好的”只查显示器”,无线鼠标和键盘膜怎么混进来了?真相:SQL里 AND 的优先级比 OR 高(跟数学里”先乘后加”一个道理),没括号时它先算 price > 1000 AND product = '显示器',再跟 price < 100 做 OR——于是”低于100″的条件变成了”不限商品”,便宜货全放行了。疫苗很简单:只要 AND、OR 同框,必加括号,把意图钉死:
# 正确:括号把"显示器里(低于100或高于1000)"的意图钉死
cur.execute("""
SELECT product, price FROM prices
WHERE product = '显示器' AND (price 1000)
""")
print(cur.fetchall())
# [('显示器', 1299.0), ('显示器', 1199.0)]
括号不收钱,意图写清楚比什么都强。
坑3:拿 = NULL 找空值,永远找不到。哪天某条记录的价格没录(NULL),你想查”哪些记录缺价格”:
cur.execute("SELECT * FROM prices WHERE price = NULL") # 永远空手而归!
cur.execute("SELECT * FROM prices WHERE price IS NULL") # 正确写法
SQL里 NULL 的意思是”未知”,”未知等于未知”在逻辑上不成立,所以 = NULL 永远不匹配任何行。找空值用 IS NULL,找非空用 IS NOT NULL。这坑不常踩、但一踩就是半小时的”我明明有数据啊”——先记住这句咒语:等于用 =,是空用 IS NULL,别串门。
七、跟AI协作的正确姿势(查询特供版)
- 报”人话问题”,让AI出SQL,你负责读懂。“prices表有product(文本)、price(小数)、checked_at(’2026-10-20 09:00’格式文本)三列。帮我查:10月下旬价格低于200的记录,只要商品和价格两列。SQL加注释解释每个子句。”——这就是本系列”说需求→读懂→改进”的循环:你出问题,AI出查询,你逐个子句验收(WHERE挑的行对不对?SELECT拿的栏够不够?)。
- 让AI”翻译”而不是”背诵”。看不懂某句SQL时,别问”什么意思”,问”把它逐词翻译成大白话,再举一个查不到任何结果的反例“。反例最养人——它逼你看见条件的边界。
- 拿错误结果回炉,比拿报错回炉更常见。查询”跑通了但结果不对”(比如坑2那道题)是本篇最常见的故障。把SQL全文+表里的几条样例数据+你期望的结果三样一起贴给AI:”实际和预期不符,帮我找条件写错了哪。”三样俱全,AI基本一次定位。
- 玩”出题互考”。“围绕SELECT和WHERE,给我出3道’把人话翻译成SQL’的题,先别给答案。”你先翻、AI再批,越练越快——毕竟查询这活儿,将来你每天都要干。
八、今日小练习
练习1:把四道”人话问题”翻译成SQL。还是练习场那11条数据,不看文章,写出能回答下列问题的SQL并跑出来验证:①一共有哪些商品(不重复)?②哪些记录的价格在200到1000之间?③10月22日(含)之后检查的记录有哪些?④名字里带”机”字的商品记录有哪些?
提示:①想想哪个关键字专治”重复”;③checked_at 是文本比较,>= '2026-10-22' 就够了,因为格式统一、字典序等于时间序;④通配符 % 要”包住”关键词才表示”包含”。
练习2:给价格监控工具加一个”查询模式”。改造你的价格监控脚本:除了记录价格,再加一个交互——用户输入一个价格上限(比如100),程序列出所有当前价格低于这个数的商品。用 WHERE price < ? 占位符写法。
提示:”当前价格”意味着每个商品只看最新一条——今天先用笨办法(把该商品所有记录查出来、Python里取最后一条)能跑通就行;高效写法是第53篇排序+分页的活儿,先留个钩子。
练习3(坑点自考):先诊断,再对答案。下面每句SQL都有毛病,不看文章说出病在哪、怎么改:①SELECT * FROM prices WHERE product = 键盘膜;②SELECT * FROM prices WHERE price 1000 AND product = '显示器'(想查”显示器里低于100或高于1000的记录”);③SELECT * FROM prices WHERE checked_at = NULL。
提示:一句对一个坑(引号、优先级、NULL);②的正确答案不止改标点——先想清楚”显示器跌破1000″这个意图到底该怎么表达。
下篇预告
下一篇(第52篇):更新和删除 — UPDATE+DELETE,让数据”改得了、删得掉”。查只是数据库的一半本事,另一半是”改”:价格录错了怎么改(UPDATE)、测试的垃圾数据怎么清(DELETE)。更关键的是——改错了怎么”只删该删的”?一条没带 WHERE 的 DELETE 能让整张表瞬间清空,第52篇教你给这头猛兽拴上绳子。
发表回复