1. 先把话说清楚:Python项目里数据库到底扮演什么角色
如果你已经跟着学习计划走到第39天,大概率能把列表、字典、函数、类玩得比较溜了,也会用文件读写保存一些数据。但你可能已经开始隐约感觉到:光靠文件存数据,好像不太够用了。比如想按条件筛选数据、想统计某个维度,每次都要把整个文件读进来再写循环,又慢又麻烦。这时候,SQL数据库就该登场了。
SQL(结构化查询语言)是操作关系型数据库的标准语言,而Python数据库操作指的是在Python代码里建立连接、执行SQL语句、读取结果的过程。两者组合起来,就是你将来写Web后端、写数据分析脚本、写自动化工具时最常用的基本功组合。这一天的内容,可以当作"从Python语法学习跨到真实项目开发"的转折点。
这篇内容适合两类人:一类是正在100天计划里学到30到50天的学习者,语法基本会了但还没碰过数据库,正好用这篇把SQL查询入门和Python数据库操作一次打通;另一类是已经会用ORM框架、但对原生SQL一直没底的人,下面的查询拆解和踩坑复盘可以帮你补上这块短板。
1.1 为什么学了几个月Python还是绕不开SQL?
很多人学Python的动力是"处理数据",但数据到底放在哪儿,大多数教程不会一开始就讲清楚。最直觉的做法是存在文本文件或JSON里,我也这么干过。写到后面你就会发现三个问题。
第一,重复读取太低效。一个几千行的JSON文件,每次程序启动都要整体load进来,哪怕你只需要其中一条记录,也得全量扫描。数据库天生就是干这个的,有索引、有缓存,查询大表能在毫秒级完成。第二,并发写入会打架。文件方式在多人同时写的时候需要自己加锁处理,处理不好就丢数据;数据库靠事务和锁机制把这个问题替我们解决掉了大半。第三,条件统计写起来极其痛苦。你试试在文件里实现"按城市分组、统计每个城市的平均消费金额",光循环和中间变量就得写一大堆,而SQL里一句SELECT加GROUP BY就结束了。
还有一个更现实的原因是:业界无论大小项目,数据层基本都落在关系型数据库上。MySQL、PostgreSQL、SQLite,它们的通信语言都是SQL。只要你想做Web开发、爬虫数据落库、数据分析ETL,就不可能完全绕开它。即使你以后用ORM,ORM把对象操作翻译成SQL、再交给数据库执行的中间步骤,依然需要你能看懂、能优化。
1.2 哪些日常场景会用到Python+SQL?
我自己的经历里,有几个特别典型的场景,几乎每个用Python的人早晚都会撞上。
爬虫落地:爬下来的数据如果只想存CSV,量小没问题;量大了之后要增量更新、去重、按时间查询,CSV就非常难受了。我习惯把清洗后的数据直接写进SQLite或MySQL,然后用SQL做后续的数据分析。
Web后端:不管用Flask还是其他框架,用户注册、商品列表、订单查询,最后都要落到数据库表上。你可能用的是ORM,但要排查数据异常、写复杂报表查询时,还是得回到SQL本身。
定时任务与报表:我做过一个每日销售汇总脚本,就是从订单表里按日期分组统计销售额,再推送出去。核心逻辑就是一条SQL,如果不会SQL,你得写一整个数据管道来模拟这条语句的行为。
自动化的数据校验:比如对比两个系统导出的数据是否一致,我写过一个脚本,分别把两边的数据灌进临时表,再用JOIN查差异,十分钟搞定,比逐行比对又快又不容易漏。
这些场景有一个共同点:核心问题是"在数据里找答案",而SQL正好是专门为这件事设计的语言。Python负责的是"拿到答案之后怎么处理、怎么展示、怎么落库",两者搭配,各干各擅长的。
2. 连接数据库的几种姿势,选错了后面的坑都是自己挖的
在动手写查询之前,先要回答一个问题:我这个场景该用哪种方式连数据库?这个选择题做对了,后面能省很多事。很多初学者拿着教程里的MySQL配置就往自己机器上套,装服务、配账号、调驱动,折腾一下午还没跑通第一条查询,其实用SQLite十分钟就能把同样的事情学明白。
2.1 内置sqlite3和第三方驱动的取舍
Python标准库里自带一个sqlite3模块,不需要装任何软件、不需要配账号密码、不需要启动服务,直接就能用。SQLite本身是一个文件型数据库,整个数据库就是硬盘上的一个文件,非常适合学习、做原型、跑本地工具。
我用一个表格把几种常见方案捋一遍,方便大家对照自己的场景选:
| 方案 | 数据库类型 | 适用场景 | 上手难度 | 备注 |
|---|---|---|---|---|
| sqlite3(标准库) | 文件型 | 学习、本地小工具、爬虫落地 | 最低,零安装 | 适合起步阶段 |
| PyMySQL | MySQL | Web后端、生产环境 | 中等,需装MySQL | 基本兼容SQL语法 |
| psycopg2 | PostgreSQL | 数据量大的业务系统 | 中等,需装PostgreSQL | 功能强、类型丰富 |
| SQLAlchemy(ORM) | 多种 | 大型项目、需要频繁改表结构 | 较高 | 本质还是翻译成SQL |
我的建议很简单:如果在学习阶段,或者项目规模不大、单机使用,优先sqlite3。它的SQL语法和MySQL、PostgreSQL高度接近,学明白之后换库基本是零成本,只是连接方式换一下。本文的实战部分也全部基于sqlite3,这样任何人都能直接跑起来,不需要先装数据库服务。
如果你明确知道以后要上MySQL,也可以直接学PyMySQL,但要注意先用本地环境把SQL基础打好,别一上来就被"建库、授权、远程连接"这些外围问题干扰了主线。我带过一位同学,他花了两天装MySQL之后,反倒把SQL本身的语法忘得差不多了。先学核心,再学外围,顺序很重要。
2.2 用ORM还是写原生SQL:别让框架替你决定
还有一个绕不开的选择:写原生SQL,还是用ORM?
ORM(对象关系映射)就是把数据库表映射成Python里的类,你以为自己在操作对象,其实背后自动生成SQL。它的好处是开发快、可读性好、换数据库不用改代码。坏处也明显:你离真实SQL越来越远,查问题的时候根本不知道后台执行了什么。
我的看法是:这个阶段,必须亲手写原生SQL。理由有三层。
第一,理解数据模型。你只有自己写JOIN、写GROUP BY,才能真正搞清楚表之间是什么关系、什么字段该建索引,这些理解是ORM给不了你的。第二,排查问题。线上SQL慢、数据对不上,最后都要落到"到底执行的是什么SQL"上。你如果看不懂执行计划,就只能抓瞎。第三,组合拳思维。复杂报表和批量更新,用ORM写反而绕,原生SQL一条搞定。等到哪天你真的要上ORM,也会知道它在背后做了什么,而不是把它当黑盒。
我自己现在写业务代码也用ORM,但排查慢查询、写复杂统计时始终用原生SQL。这不是二选一的问题,而是不同层次用不同工具。先掌握底层那个,上层那个就是锦上添花。
3. SQL查询基本功:把SELECT这条主线彻底走通
数据库操作里,最常用、最核心的就是查询,而查询的入口只有一个:SELECT语句。这一节我带着大家把SELECT从最简单到稍复杂逐层走一遍,每一步都会有真实可跑的SQL示例。
为了后面的演示有个统一环境,我先把示范用的两张表定义出来。一张是用户表,一张是订单表,它们通过user_id关联,这也是后面讲JOIN的素材。
-- 用户表 CREATE TABLE user_info ( user_id INTEGER PRIMARY KEY, name TEXT NOT NULL, city TEXT, age INTEGER, register_date TEXT ); -- 订单表 CREATE TABLE orders ( order_id INTEGER PRIMARY KEY, user_id INTEGER, product_name TEXT, amount REAL, order_date TEXT );字段都不复杂,含义一目了然。下面所有查询都在这两张表上跑。
3.1 单表查询:过滤、排序、去重、限量
最基础的查询是整表查全。生产环境里一般没人敢直接这么干,因为数据量大时会出问题,但学习阶段没问题:
SELECT * FROM user_info;*表示所有列。实际写代码时我几乎不用*,因为只取需要的列,一方面传输数据量小,另一方面代码意图也更清楚。
过滤条件写在WHERE后面,支持组合判断。例如找出上海、年龄大于25的用户:
SELECT name, city, age FROM user_info WHERE city = '上海' AND age > 25 ORDER BY age DESC LIMIT 10;这里一次出现了三个常用子句:WHERE做过滤、ORDER BY ... DESC做降序排序、LIMIT限量只取前10条。LIMIT在开发和调试时特别有用,先跑通小样本,确认结果没问题再放开限制。
去重用DISTINCT,比如统计"订单表里出现过哪些商品名":
SELECT DISTINCT product_name FROM orders;模糊匹配用LIKE加通配符,%表示任意长度的字符串。比如搜索名字里带"花"的用户:
SELECT name FROM user_info WHERE name LIKE '%花%';初学阶段容易忽略一个点:WHERE子句的执行顺序在SELECT列之前,也就是说,WHERE里不能引用你在SELECT里起的别名。这是我当年常犯的错误,后面第6节还会专门提。
3.2 聚合统计:GROUP BY和HAVING的正确打开方式
单行查询会之后,下一个台阶是聚合统计。聚合函数把多行结果合并成一行,最常用的有COUNT(计数)、SUM(求和)、AVG(平均)、MAX(最大)、MIN(最小)。
比如统计用户总数、平均年龄、最大年龄:
SELECT COUNT(*) AS total_users, AVG(age) AS avg_age, MAX(age) AS max_age FROM user_info;聚合跟分组搭配才真正强大。按城市统计用户数,并只保留用户数超过1的城市:
SELECT city, COUNT(*) AS cnt FROM user_info GROUP BY city HAVING COUNT(*) > 1;这里有一个非常关键的区分:WHERE是在分组之前过滤原始行,HAVING是在分组之后过滤聚合结果。拿"城市"举例就是:先删掉不符合条件的用户,再分组统计,这叫WHERE;先分组统计出每个城市的人数,再删掉人数少的城市,这叫HAVING。顺序不同,结果完全不同。
我见过很多初学者在这上面栽跟头,最典型的错误是写WHERE COUNT(*) > 1,然后报错找不到原因。记住一句话:涉及聚合结果的过滤,必须用HAVING。
3.3 多表关联:JOIN的两种最常见形态
真实业务的数据不太可能都放在一张表里。用户信息和订单信息拆开存,是为了避免重复存储和修改异常。要在查询时把它们拼起来,就用JOIN。
最常用的是INNER JOIN,只返回两边都能匹配上的行。比如查每个订单对应用户的城市:
SELECT o.order_id, u.name, o.product_name, o.amount FROM orders o INNER JOIN user_info u ON o.user_id = u.user_id;注意我给两张表起了别名o和u,这是写JOIN的习惯,代码短、可读性好,特别是字段多的时候。
另一种形态是LEFT JOIN,左表的所有行都保留,右表没有匹配的则为NULL。这和INNER JOIN的差异在真实场景中非常关键。比如我想知道"每个用户买了哪些商品",订单表里没有记录的用户也要列出来,只是商品为NULL:
SELECT u.name, o.product_name FROM user_info u LEFT JOIN orders o ON u.user_id = o.user_id ORDER BY u.user_id;LEFT JOIN最常见的坑是:因为它会把左表的行复制多份(一对多时),所以统计结果会被放大。比如JOIN完再COUNT(*),数出来的行数可能比实际用户数多。如果只想判断"有没有订单",通常用EXISTS更合适,这个细节以后写复杂查询时很容易中招。先记住:JOIN之后的行数可能膨胀,做统计前要想清楚这是不是你想要的。
4. 在Python里执行SQL的正确姿势:连接、游标与参数化
SQL本身会写之后,下一步就是把语句放进Python代码里真正跑起来。这一步技术含量不高,但细节决定成败。我见过太多人把一套能跑的查询,因为连接管理没做好、参数传递方式不对,在生产环境埋了一堆雷。
4.1 连接和游标的生命周期管理
在Python里用sqlite3操作数据库,标准流程是三步:建立连接、创建游标、执行语句。游标(cursor)可以理解成一个"结果指针",数据库执行完SQL后,返回的结果通过游标来读取。
基础写法是这样的:
import sqlite3 conn = sqlite3.connect('demo.db') cursor = conn.cursor() cursor.execute("SELECT name, age FROM user_info WHERE city = ?", ('北京',)) rows = cursor.fetchall() cursor.close() conn.close()几个细节值得注意。
第一,sqlite3.connect('demo.db')如果文件不存在会自动创建,这给学习省了很多事。但生产环境连MySQL时,连接参数会更复杂,这里先不展开。
第二,执行完毕要关游标、关连接。很多人图省事只关连接不关游标,短期没事,连接池场景下会积累隐患。更好的做法是用上下文管理器:
import sqlite3 with sqlite3.connect('demo.db') as conn: cursor = conn.cursor() cursor.execute("SELECT name FROM user_info WHERE city = ?", ('北京',)) rows = cursor.fetchall()with块结束会自动提交或回滚事务,但注意它并不会自动关闭游标。最稳妥的写法是配合contextlib.closing把游标也纳入管理,或者至少在函数末尾显式close。我在带人时会给一个原则:谁打开谁负责关闭,没有例外。
第三,fetchall()会把所有结果一次性读进内存。数据量小没问题,几百万行就别这么干了,后面4.3小节会给出应对方案。
4.2 为什么绝不能用字符串拼接SQL
这是数据库操作里最不能妥协的一条红线。
初学者最容易写出这样的代码:
# 错误示范,千万别学 user_input = input("请输入用户名:") cursor.execute("SELECT * FROM user_info WHERE name = '" + user_input + "'")看起来简单直接,但用户输入一旦变成SQL的一部分,灾难就来了。假设用户输入的是:
'; DROP TABLE user_info; --拼接后的SQL就变成了多条语句,where条件前面被强制结束,后面跟着删表指令。这就是SQL注入,业界每天都在发生的安全漏洞,不是危言耸听。
正确做法是用参数占位符:
user_input = input("请输入用户名:") cursor.execute( "SELECT * FROM user_info WHERE name = ?", (user_input,) )sqlite3的占位符是?,MySQL的PyMySQL库用的是%s,PostgreSQL的psycopg2也用%s。写法略有差异,但原理相同:数据库驱动会把参数当成"值"来处理,而不是拼进SQL文本,注入代码就失去了语义。
我自己的底线是:任何动态传入的变量,一律走参数化,哪怕它看起来人畜无害。因为代码写多了,你根本无法保证半年后的自己还记得这里曾经拼接了什么。
4.3 结果集的三种取法,按场景选
fetchall()一次性取回所有行,适合小结果集。另外还有两个方法:fetchone()取得下一行,fetchmany(n)一次取n行。
三种方法的使用场景差别很明显。
如果查询结果预期很小(比如按主键查一条),fetchall()最省事。如果结果很大(比如几十万行),一次性载入内存会拖垮程序,正确的做法是分批读取:
cursor.execute("SELECT order_id, amount FROM orders") while True: batch = cursor.fetchmany(500) if not batch: break # 对这批数据做处理 print(len(batch))fetchone()适合"只需要一条记录"的场景,比如按主键取用户详情。它还有个别名逻辑:循环调用fetchone()直到返回None,也能实现逐行遍历,但相比fetchmany(500)逐批取,性能差距明显。
这里多提一句游标的一个重要特性:同一个连接上的游标是有状态的,它记录了当前读到了哪一行。如果同一个游标还没读完整结果就去执行别的SQL,会发生无法预期的行为。所以不同用途的查询,尽量各开各的游标,用完即关。
5. 实战带练:用Python+SQL做一套图书库存统计Demo
前面四节的工具都齐了,现在把它们组合成一个小项目。我选的是图书库存统计,因为这个场景贴近生活、数据关系清晰,又能把过滤、聚合、JOIN、参数化全部用上。完整代码我会贴出来,大家可以照着敲一遍,再自己加一两个查询练手。
5.1 建表与造数据:先把测试环境搭起来
项目目标:有一批图书数据和库存数据,需要回答几个问题:各类图书的平均价格、库存不足的书有哪些、每个出版社的库存总量、按书名搜索。
我用两张表。books存图书基本信息,stock存库存量:
CREATE TABLE books ( book_id INTEGER PRIMARY KEY, title TEXT NOT NULL, author TEXT, category TEXT, price REAL, publisher TEXT ); CREATE TABLE stock ( book_id INTEGER PRIMARY KEY, quantity INTEGER, warehouse TEXT );为了演示效果,我往里插10本书的数据。真实项目里数据来自录入或爬虫,这里手动插入是为了让大家看得清楚:
import sqlite3 books_data = [ (1, 'Python入门教程', '张三', '编程', 59.0, '某邮电出版社'), (2, 'SQL实战指南', '李四', '编程', 45.0, '某机械工业出版社'), (3, '数据结构与算法', '王五', '计算机', 78.0, '某大学出版社'), (4, '漫画算法', '赵六', '计算机', 39.9, '某邮电出版社'), (5, '小王子', '圣埃克苏佩里', '文学', 28.0, '某文学出版社'), (6, '百年孤独', '马尔克斯', '文学', 55.0, '某出版公司'), (7, '三体', '刘慈欣', '科幻', 88.0, '某出版社'), (8, '球状闪电', '刘慈欣', '科幻', 45.0, '某科学技术出版社'), (9, '人类简史', '赫拉利', '历史', 68.0, '某出版社'), (10, '明朝那些事儿', '当年明月', '历史', 39.0, '某人民出版社'), ] stock_data = [ (1, 120, '北京仓'), (2, 8, '北京仓'), (3, 0, '上海仓'), (4, 60, '上海仓'), (5, 200, '广州仓'), (6, 150, '广州仓'), (7, 5, '北京仓'), (8, 90, '上海仓'), (9, 3, '广州仓'), (10, 180, '北京仓'), ] conn = sqlite3.connect('library.db') cursor = conn.cursor() cursor.execute(""" CREATE TABLE IF NOT EXISTS books ( book_id INTEGER PRIMARY KEY, title TEXT NOT NULL, author TEXT, category TEXT, price REAL, publisher TEXT ) """) cursor.execute(""" CREATE TABLE IF NOT EXISTS stock ( book_id INTEGER PRIMARY KEY, quantity INTEGER, warehouse TEXT ) """) cursor.executemany("INSERT INTO books VALUES (?, ?, ?, ?, ?, ?)", books_data) cursor.executemany("INSERT INTO stock VALUES (?, ?, ?)", stock_data) conn.commit() cursor.close() conn.close()executemany是一次把多条数据传进去批量执行,比循环execute快得多。插入之后记得conn.commit()提交,否则关闭连接时数据会丢。这也是第6节要专门讲的第一个坑。
5.2 核心查询逐一实现并拆解
第一个问题:每类图书的平均价格和数量。这就是第3节的聚合查询:
import sqlite3 with sqlite3.connect('library.db') as conn: cursor = conn.cursor() cursor.execute(""" SELECT category, COUNT(*) AS cnt, ROUND(AVG(price), 2) AS avg_price FROM books GROUP BY category """) for row in cursor.fetchall(): print(row)输出大概是('编程', 2, 52.0)这样的结果。注意我用了ROUND(..., 2)把平均价保留两位小数,这是报表类查询的常见处理。COUNT的语义要牢记:它统计的是行数,如果某列有NULL,COUNT(列名)不会把它算进去,但COUNT(*)会。这两个写错会导致数字对不上。
第二个问题:库存不足警戒线(低于10本)的书名和数量。这里涉及JOIN和WHERE的组合:
cursor.execute(""" SELECT b.title, s.quantity, s.warehouse FROM books b INNER JOIN stock s ON b.book_id = s.book_id WHERE s.quantity < 10 ORDER BY s.quantity ASC """)从输出能明显看到,库存为0的书也排在前面。这就是INNER JOIN的语义:如果某本书在stock表里没有对应记录,直接过滤掉不显示,业务上这往往就是"这本书没入库"的坏信号。如果想查出"根本就没库存记录的书",就要换成LEFT JOIN加WHERE s.book_id IS NULL。这两种写法的业务含义完全不同,写报表的时候先想清楚你要哪一种。
第三个问题:按出版社统计库存总量。SUM配合GROUP BY加JOIN,两张表连起来后聚合:
cursor.execute(""" SELECT b.publisher, SUM(s.quantity) AS total_stock FROM books b LEFT JOIN stock s ON b.book_id = s.book_id GROUP BY b.publisher ORDER BY total_stock DESC """)我在这条查询里特意用了LEFT JOIN,因为如果某本书没有库存记录,INNER JOIN会在聚合前把它丢掉,导致出版社统计缺失;LEFT JOIN会保留它,配合COALESCE(s.quantity, 0)可以把NULL当成0处理。聚合结果差这么一点点,在报表里就是事故。
5.3 把查询封装成函数后的复用心得
上面几段代码能跑,但都是散的。实际项目里,我会把每个业务查询封装成函数,对外只暴露数据和参数,不让连接细节漏出去。比如按书名模糊搜索和按出版社找库存:
import sqlite3 DB_PATH = 'library.db' def search_books(keyword): conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute(""" SELECT book_id, title, author, price FROM books WHERE title LIKE ? """, (f'%{keyword}%',)) rows = cursor.fetchall() cursor.close() conn.close() return rows def stock_by_publisher(): conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute(""" SELECT b.publisher, SUM(COALESCE(s.quantity, 0)) FROM books b LEFT JOIN stock s ON b.book_id = s.book_id GROUP BY b.publisher """) rows = cursor.fetchall() cursor.close() conn.close() return rows封装的好处是调用方根本不需要知道SQL长什么样,只要传参数就能拿结果。这看起来简单,但有几个值得注意的点。
第一,LIKE参数化也是可以的,用f'%{keyword}%'拼出带通配符的参数,然后传给占位符。这样既保留模糊匹配的能力,又不会引入注入风险。第二,我每写一个查询函数都单独管理连接和游标,函数内开、函数内关。虽然每次都重复,但保证资源一定被释放。第三,真正生产环境里,连接创建很贵,通常会用连接池或全局复用连接,局部场景可以先把"每次新建连接"跑通,以后再优化。先正确,再高效,这个顺序不能反。
6. 我在实践中踩过的五个坑,每一个都值得你记下来
前五节的内容大多是"该怎么做",这一节我专门聊聊"我原来是怎么做错的"。这些坑没有一个是什么高深原理,但几乎每个初学者都躲不开。我在带人的时候发现,把这些坑讲透,比再讲十个SQL语法都管用。
6.1 事务没提交,数据"消失"了
我第一次用sqlite3往表里插数据,插完连接一关,再打开一看,表是空的。我当时以为是写入失败,折腾了半天才发现,原来是忘了调conn.commit()。
原因要讲清楚:sqlite3默认每执行一条INSERT或UPDATE都在一个隐式事务里,只有commit之后才会真正落盘。如果你忘了commit就直接close,变更会被回滚。Python的with sqlite3.connect(...) as conn:块结束时,如果块内没有异常,会自动commit;如果有异常,会自动rollback。这个行为一开始容易误判,以为with块一退出数据就保存了,其实只有正常退出才提交。
我的经验是:写任何"改数据"的代码,提交动作要写得很显眼。要么在函数最后一行明确调conn.commit(),要么把数据库操作包在try/except里,成功就commit、失败就rollback并打印错误。最忌讳的是把commit藏在很深的逻辑里,出问题时找都找不到。
6.2 N+1查询:看起来没毛病,一跑就卡
N+1是指:先查一次拿到N条主记录,然后循环里对每一条各查一次关联表。N越大,查询次数越多,性能指数级恶化。这个坑在ORM时代特别常见,在原生SQL里也可能踩。
一个典型场景:查所有用户,然后循环里查每个用户的订单数。写成代码是这样的:
# 错误示范:N+1 cursor.execute("SELECT * FROM user_info") users = cursor.fetchall() for u in users: cursor.execute("SELECT COUNT(*) FROM orders WHERE user_id = ?", (u[0],)) print(cursor.fetchone())如果用户有1000个,这就是1001次查询。正确姿势是用一次JOIN加GROUP BY把数据全部算出来:
cursor.execute(""" SELECT u.name, COUNT(o.order_id) FROM user_info u LEFT JOIN orders o ON u.user_id = o.user_id GROUP BY u.user_id """)判断自己是不是踩了N+1的简单方法:看代码里有没有"查询循环"。有的话,优先考虑能不能合并成一次。这不是教条,是数据库的基本使用习惯。
6.3 中文乱码:源头在连接,不在展示
我曾经在本地写脚本往数据库存中文,存进去之后查出来全是乱码。我第一反应是改打印、改编码声明,折腾很久才发现问题根本不在那。
sqlite3对中文没有编码问题,因为数据在文件里就是文本。但如果用的是MySQL,连接层面的charset没设对,中文就真的会乱。解决办法是在连接时显式指定charset,比如PyMySQL的连接参数里传charset='utf8mb4'。另一个很容易忽略的点是:数据库表本身的字符集也要一致,建库时最好统一utf8mb4。
给初学者的建议:乱码排查的顺序是"存储层、连接层、展示层",一层层确认,不要一上来就改打印函数。先在数据库客户端里直接跑SQL看结果,如果客户端显示正常,那问题一定在代码的连接或取值环节。
6.4 忘记关闭资源:短时间没事,时间长了必炸
连接和游标不关闭,短脚本跑完进程退出会自动回收,看起来没啥影响。但换到长驻服务(比如Web应用)里,连接一直不释放,数据库连接数会迅速被占满,新的请求全部排队或直接报错。
在Python里,最可靠的资源管理方式就是上下文管理器。标准库为sqlite3的连接提供了上下文协议,配合closing可以连游标一起管理:
from contextlib import closing import sqlite3 with closing(sqlite3.connect('demo.db')) as conn: with closing(conn.cursor()) as cursor: cursor.execute("SELECT 1")如果团队代码风格不统一,我建议至少在函数级约定:连接变量只在函数内使用,谁创建谁关闭,绝对不把连接对象透传到其他模块。这条约定救过我很多次。
6.5 索引缺失:数据量一大,查询立刻变慢
学习阶段数据量小,全表扫描毫无感觉。但到了真实数据,一张表几十万行,没有索引的查询可能从毫秒级变成秒级甚至分钟级。我曾经给某张表写查询,一开始好好的,数据量涨到几十万后,按日期区间的查询慢到无法接受,一查执行计划,才发现连索引都没建。
索引的原理可以简化理解为书的目录:没有目录就得一页页翻,有目录就能直接翻到相应章节。在SQL层面,最常用的是单列索引:
CREATE INDEX idx_orders_user_id ON orders(user_id); CREATE INDEX idx_orders_order_date ON orders(order_date);给哪列建索引,原则很简单:WHERE和JOIN里经常用来过滤的列,优先考虑。但索引不是越多越好,每个索引都会拖慢写入速度、占额外空间。先跑通查询、确认SQL本身没毛病、再根据实际慢查询加索引,这个顺序很重要。很多初学者一上来就到处建索引,结果写入慢成蜗牛。
还有一个小技巧:排查慢查询时,让数据库解释执行计划。sqlite3里可以这样做:
cursor.execute("EXPLAIN QUERY PLAN SELECT ...") print(cursor.fetchall())看到SCAN字样就是全表扫描,看到SEARCH才是用了索引。这个习惯早养成早受益。
回头再看,这一天的核心其实就两件事:SQL能帮你高效地向数据提问,Python负责把问题和答案衔接起来。我个人带人学到这里时,总会叮嘱一句:别急着追求复杂的SQL技巧,先把单表查询、聚合、JOIN这三块练熟,配上一套参数化操作的肌肉记忆,后面无论换MySQL还是PostgreSQL,都只是换一层连接壳而已。数据库这块,基础越扎实,后面越省心。