☰
Python必学:SQL查询入门与sqlite3数据库操作实战
2026/10/14 4:29:04 网站建设 项目流程

1. 先说两句大实话:为什么SQL是Python绕不过去的坎

我从几年前开始带Python入门训练营,每次到“数据库操作”这一天,总会看到有人嘀咕:我只是想写个脚本处理Excel,为什么要学SQL?等到实际做项目才发现,用户信息、订单记录、日志状态、商品库存,随便一个后台系统的核心数据都放在数据库里。你Python写得再花哨,取不到数据就等于没有地基。day39这个节点安排得很聪明,前面三十多天已经把语法、函数、文件操作、面向对象都过了一遍,这时候把SQL查询入门和Python数据库操作串起来,刚好让你具备“自己从零搭一个带持久化存储的小系统”的能力。

这一天的内容,本质上就是两件事:第一,学会用SQL语言向数据库问问题;第二,学会用Python把这些问题发出去、把结果收回来。你不用成为数据库专家,也不需要背各种底层原理,但必须搞清楚连接、游标、执行、提交、关闭这条主链路,能做到“看了一眼别人的代码,知道它在干嘛”,并且能自己动手建一张表、插入几条数据、再把它查出来。前面学的函数、异常处理和文件读写,在这节课里全部派上用场,有一种“之前那些知识终于串起来”的感觉。

适合来看这篇笔记的人,我大概分三类:刚写完基础语法、想找个实际场景练手的Python初学者;已经写过爬虫或数据处理脚本、但一直在用CSV/Excel、总感觉撑不起小项目的同学;以及做AI或自动化方案时,需要快速理解数据库代码含义的从业者。如果你已经对SQL非常熟练,可以直接跳到第三节看Python操作部分的坑点,第四节也有一个能直接照抄的完整案例。

2. SQL查询入门:先学会“问数据库要数据”的正确姿势

2.1 把SQL查询当成“做填空题”

刚接触SQL时最容易犯的错,是一上来就背各种子句的用法,结果查条件一多就晕。我教过很多学员后发现,把SQL查询理解成“填空题”更管用。标准的查询语句长这样:

SELECT 字段1, 字段2 FROM 表名 WHERE 条件 ORDER BY 排序字段 LIMIT 数量;

你要做的,就是先明确“我要从哪张表拿数据”,再想“要哪几个字段”,最后才是“筛选什么条件”。很多人习惯先写WHERE再写FROM,其实SQL真正执行的顺序并不是你写代码的顺序。数据库引擎会先定位到 FROM 指定的那张表,然后按 WHERE 把行筛一遍,再去计算 SELECT 后面的字段,最后才做排序和分页。理解这个顺序,遇到“为什么这个字段不能在WHERE里用别名”之类的问题就不迷茫了。

2.2 最常用的查询子句逐个过一遍

先给一个最基础的例子。假设我们有一张用户表users,里面有id、name、city、age四个字段:

SELECT id, name FROM users WHERE city = 'Shanghai' ORDER BY age DESC LIMIT 20;

这条语句的意思是:从用户表里,把居住在上海的用户的编号和姓名取出来,按年龄从大到小排,最多取二十行。这里有几个细节值得新手注意:字符串条件要加单引号,字段名和表名在大小写不敏感的时候可以顺手统一,但别指望所有数据库都那么宽松;LIMIT在MySQL、SQLite、PostgreSQL里都能用,但SQL Server用的是TOP,Oracle用的是FETCH FIRST,跨数据库迁移时容易踩坑。

日常写查询时,我强烈建议每写一条都先在数据库工具里跑一遍,确认结果没问题再放到Python代码里。数据库工具很多,随手挑一个顺手的图形客户端就行,操作逻辑都差不多。你不需要背几十条SQL语法,只需要把SELECT、FROM、WHERE、ORDER BY、LIMIT这五个基础件用熟,后面什么IN、LIKE、BETWEEN、GROUP BY都是往上叠条件而已。

2.3 JOIN、聚合、分组,一次说清

只查一张表解决不了真实问题。比如我想看“每个用户最近一次登录时间”,用户基本信息和登录日志通常放在两张表里。这时需要把两张表按用户ID拼起来,这就是JOIN。先看一个概念:JOIN后面跟着ON,表示按哪个字段对齐。

SELECT users.name, login_logs.last_login_time FROM users LEFT JOIN login_logs ON users.id = login_logs.user_id;

LEFT JOIN的意思是以左表(users)为主,左表所有用户都保留,能匹配上登录日志的就把时间带出来,匹配不上的显示为NULL。比它严格的是INNER JOIN,只保留两边都能匹配上的行。新人一开始不用纠结那么多,记住:做报表、做明细查询时,左表通常是“主体”,所以LEFT JOIN用得最多。

聚合和分组是另一个高频知识点。比如统计每个城市的用户数:

SELECT city, COUNT(*) AS user_count FROM users GROUP BY city ORDER BY user_count DESC;

这里COUNT(*)是聚合函数,GROUP BY city表示按城市分组。聚合函数只能在分组或整体查询里出现,新手最容易犯的错是“在SELECT里写了非聚合字段,但GROUP BY没包含它”,这在很多数据库里会直接报错,在SQLite里比较宽容,但在MySQL的默认配置下就很容易踩雷。建议一开始就养成“SELECT后面写什么,GROUP BY后面就尽量跟什么”的习惯,后面会省很多事。

2.4 参数化查询:新手必须当天就养成的习惯

如果现在有人告诉你,从数据库取数据的SQL语句可以这样写:

cursor.execute("SELECT * FROM users WHERE name = '" + username + "'")

请你直接忘掉。这种字符串拼接方式不仅容易因为引号、特殊字符出各种格式问题,更重要的是存在注入风险。所谓注入,就是用户在输入框里提交了一段精心构造的内容,把你原来的SQL语句“改写”了。最典型的例子是:

' OR '1'='1

拼进SQL之后,原来的“查一个用户”就变成了“查出所有用户”。如果你在登录功能里这么写,等于把整个用户表都暴露在外面。解决方式非常简单,使用参数化查询,把用户输入当作“参数”传进去,而不是当作SQL代码的一部分:

cursor.execute("SELECT * FROM users WHERE name = ?", (username,))

SQLite用问号做占位符,MySQL的驱动通常用%s,PostgreSQL的psycopg2用%s或者更严格的格式。无论哪种,核心思路都一样:数据和SQL语句分开。这个习惯最好在第一天接触数据库时就养成,不然后面代码写得越多,隐患越大。

3. Python操作数据库:连库、读写、提交、关闭,一个都不能少

3.1 环境准备与库选型

Python操作数据库,第一步是选驱动。这里我先推荐大家学习阶段只用标准库sqlite3,因为它不需要安装任何额外的东西,Python自带,数据就是一个本地文件,想删就删,想拷就拷,特别适合反复练习。等搞懂了完整流程,再切到MySQL或PostgreSQL时,你发现套路其实一模一样,因为Python官方的DB-API规范把这类接口统一了。

如果你在Linux环境,顺手装了Python,那直接打开交互模式就能试。Windows也一样,内置的Python环境中就带sqlite3。如果后期接入MySQL,通常会用PyMySQL这个纯Python库,或者是mysql-connector-python;接入PostgreSQL则常用psycopg2。安装命令到处都有,核心其实不是装包,而是明白“不同数据库只是换了连接参数和少量SQL语法,操作框架并不变”。

3.2 连接、游标、执行、提交、关闭的标准流程

直接看代码,这是最基本的连接SQLite数据库并执行查询的流程:

import sqlite3 # 1. 建立连接 conn = sqlite3.connect("demo.db") # 2. 创建游标 cursor = conn.cursor() # 3. 执行SQL cursor.execute("SELECT 1, 'hello'") # 4. 获取结果 row = cursor.fetchone() print(row) # 5. 关闭游标和连接 cursor.close() conn.close()

这里每个步骤都有它存在的理由。连接对象代表了你和数据库之间的“会话通道”,游标则像是通道里的“指针”,用来一句句执行SQL并取回结果。有人觉得多此一举,但实际开发中,你完全有可能在同一个连接上开多个游标,执行不同任务,互不干扰。

第4步获取结果有三种常用方法:fetchone()拿一行,fetchall()拿全部行,fetchmany(size)分批拿。拿回来的结果默认是元组类型,比如(1, 'hello'),如果你希望按列名访问,可以在建立连接时加一行:

conn.row_factory = sqlite3.Row

这样取出来的行就支持用row["列名"]来访问,代码可读性高不少。学习阶段建议就把这个设置成习惯。

3.3 写数据、读数据、改数据、删数据全流程实战

查询只是数据库操作的一半,真正的日常开发还得写入、修改和删除。看这段完整代码,我演示了建表、插入、查询、更新、删除的全过程:

import sqlite3 conn = sqlite3.connect("user_demo.db") conn.row_factory = sqlite3.Row cursor = conn.cursor() # 建表 cursor.execute(""" CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, city TEXT ) """) # 插入数据 cursor.execute("INSERT INTO users (name, city) VALUES (?, ?)", ("阿明", "杭州")) cursor.execute("INSERT INTO users (name, city) VALUES (?, ?)", ("小悦", "南京")) # 注意!这里必须提交 conn.commit() # 查询数据 cursor.execute("SELECT id, name, city FROM users WHERE city = ?", ("杭州",)) rows = cursor.fetchall() for row in rows: print(row["id"], row["name"], row["city"]) # 更新数据 cursor.execute("UPDATE users SET city = ? WHERE name = ?", ("苏州", "阿明")) conn.commit() # 删除数据 cursor.execute("DELETE FROM users WHERE name = ?", ("小悦",)) conn.commit() cursor.close() conn.close()

运行时的两个关键点需要多说几句。

第一,执行写操作(增删改)之后必须调用conn.commit()才会真正生效。很多新手写完execute之后直接去查,发现数据还是空的,大概率就是忘了提交。理解起来也不难:连接对象默认开启了事务,execute只是把操作放进“缓冲区”,只有commit()才把事务落盘。如果你不想手动管理,可以用上下文管理语法:

with sqlite3.connect("user_demo.db") as conn: cursor = conn.cursor() cursor.execute("INSERT INTO users (name, city) VALUES (?, ?)", ("阿强", "广州")) # 退出 with 块时自动 commit,如果中途异常则自动 rollback

这样写少了两行代码,而且异常时回滚的行为更安全。

第二,增删改操作之前最好先把SQL写对。虽然提交之前理论上还能回滚,但多数时候你也不知道该回滚到哪个阶段,与其事后补救,不如执行之前多检查一遍条件里的字段名和参数是否匹配。

3.4 事务与异常处理:什么时候该回滚

事务这个概念听起来高大上,其实生活里全是例子。你用手机银行转账,扣款和到账必须同时成功或同时失败,不能出现钱扣了但对方没收到的情况。数据库事务就是干这个的。Python的DB-API里,事务的提交靠commit(),回滚靠rollback()。

实际项目里,我建议养成“批量操作必须包在异常处理里”的习惯:

try: cursor.execute("UPDATE accounts SET balance = balance - 100 WHERE id = ?", (1,)) cursor.execute("UPDATE accounts SET balance = balance + 100 WHERE id = ?", (2,)) conn.commit() except Exception: conn.rollback() print("事务已回滚")

这种写法保证任何一步出错,前面的操作都不会残留。SQLite对单连接写操作有锁机制,特别是多个程序同时写同一个库文件时会出现“database is locked”的报错。碰到这种情况,可以适当加连接超时参数,比如:

conn = sqlite3.connect("user_demo.db", timeout=10)

意思是如果数据库被其他连接占用,最多等十秒。日常练习只要并发不高基本遇不到,但写多线程或异步程序时会非常管用。

4. 一个能直接抄作业的实操案例:用Python做图书借阅查询

4.1 需求拆解与表结构设计

理论说完,拿一个具体场景练手。假设某小组项目需要一个图书借阅查询功能,要求能查书、能查某人借了什么书、能看到每个月的借阅量。我设计两张表:一张是书籍信息表books,另一张是借阅记录表borrow_records。书籍表记录书的基本信息,借阅表记录谁在什么时候借了哪本书。表结构如下:

CREATE TABLE books ( book_id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, author TEXT, total_copies INTEGER DEFAULT 1 ); CREATE TABLE borrow_records ( record_id INTEGER PRIMARY KEY AUTOINCREMENT, book_id INTEGER NOT NULL, borrower TEXT NOT NULL, borrow_date TEXT NOT NULL );

这里用TEXT存日期,取“YYYY-MM-DD”格式,排序、比较都很方便。为什么要拆两张表?因为如果一张表里同时塞书籍信息和借阅记录,那么一本书被借了十次就要重复存十遍书名和作者,数据冗余不说,改个作者还要把所有行都改一遍。拆开之后,借阅记录只存book_id,通过关联去查书名,这就是第三章讲的JOIN发挥作用的地方。

4.2 建表、初始化和填充测试数据

写一个初始化脚本:

import sqlite3 conn = sqlite3.connect("library.db") cursor = conn.cursor() cursor.execute(""" CREATE TABLE IF NOT EXISTS books ( book_id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, author TEXT, total_copies INTEGER DEFAULT 1 ) """) cursor.execute(""" CREATE TABLE IF NOT EXISTS borrow_records ( record_id INTEGER PRIMARY KEY AUTOINCREMENT, book_id INTEGER NOT NULL, borrower TEXT NOT NULL, borrow_date TEXT NOT NULL ) """) # 填充书籍数据 books_data = [ ("Python编程入门", "某作者甲", 3), ("SQL基础教程", "某作者乙", 2), ("数据分析实战", "某作者丙", 1), ] cursor.executemany( "INSERT INTO books (title, author, total_copies) VALUES (?, ?, ?)", books_data ) # 填充借阅数据 records_data = [ (1, "阿明", "2024-11-02"), (1, "小悦", "2024-11-02"), (2, "阿明", "2024-11-03"), (3, "小悦", "2024-12-01"), ] cursor.executemany( "INSERT INTO borrow_records (book_id, borrower, borrow_date) VALUES (?, ?, ?)", records_data ) conn.commit() cursor.close() conn.close() print("初始化完成")

这里有两点经验值得分享。其一,批量插入用executemany比循环execute高效得多,尤其是数据到几千行时区别非常明显。其二,初始化脚本和业务代码最好分开,不要每次运行时都重新建表、都插入同样的测试数据,否则调试时会分不清哪条数据是自己插入的、哪条是历史残留。我自己的习惯是单独写一个init_db.py,只在必要时执行。

4.3 查询端:三个带参数的具体查询函数

接下来写业务查询部分。第一个需求,按书名查书籍信息和可借数量:

def search_books_by_title(keyword): conn = sqlite3.connect("library.db") conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute( "SELECT book_id, title, author, total_copies FROM books WHERE title LIKE ?", (f"%{keyword}%",) ) rows = cursor.fetchall() cursor.close() conn.close() return rows

这里用LIKE实现模糊搜索,用户在搜索框里输入“Python”,就能把书名里包含“Python”的书全部带出来。参数化查询里的f"%{keyword}%"仍然把整个字符串作为参数传进SQL,不要直接拼到SQL里。

第二个需求,查某个人的所有借阅记录,需要显示书名和借书日期:

def get_borrow_records_by_person(name): conn = sqlite3.connect("library.db") conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute(""" SELECT b.title, b.author, br.borrow_date FROM borrow_records br JOIN books b ON br.book_id = b.book_id WHERE br.borrower = ? ORDER BY br.borrow_date DESC """, (name,)) rows = cursor.fetchall() cursor.close() conn.close() return rows

第三个需求,统计每个月的借阅量:

def get_monthly_borrow_stats(): conn = sqlite3.connect("library.db") conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute(""" SELECT substr(borrow_date, 1, 7) AS month, COUNT(*) AS cnt FROM borrow_records GROUP BY month ORDER BY month """) rows = cursor.fetchall() cursor.close() conn.close() return rows

这里用substr(borrow_date, 1, 7)把2024-11-02截取成2024-11,字符串截取在SQLite里很好用,如果是MySQL,对应的是LEFT(borrow_date, 7),PostgreSQL则可以用TO_CHAR(borrow_date, 'YYYY-MM')或DATE_TRUNC,不同数据库略有差异。

4.4 运行效果与数据验证

写一段调用逻辑,验证上面几个函数的输出:

if __name__ == "__main__": print("按书名搜索:") for row in search_books_by_title("Python"): print(row["book_id"], row["title"], row["author"], row["total_copies"]) print("阿明的借阅记录:") for row in get_borrow_records_by_person("阿明"): print(row["title"], row["author"], row["borrow_date"]) print("每月借阅量:") for row in get_monthly_borrow_stats(): print(row["month"], row["cnt"])

我实际跑出来的结果是:

按书名搜索: 1 Python编程入门 某作者甲 3 3 数据分析实战 某作者丙 1 阿明的借阅记录: SQL基础教程 某作者乙 2024-11-03 Python编程入门 某作者甲 2024-11-02 每月借阅量: 2024-11 3 2024-12 1

验证时有两个容易忽视的小问题:一是book_id自增后不一定是插入顺序,删除过数据就会跳号,这不影响查询;二是搜索关键词“Python”时,数据分析实战也匹配出来了,因为书名里包含了“Python”这个关键词,如果只想匹配开头,就把%keyword%改成keyword%。

5. 常见问题与排查技巧实录

5.1 “连接不上数据库”的排查顺序

这段基本都是我在答疑时反复遇到的真实问题。连不上SQLite数据库文件,最常见的原因不是密码或权限,而是路径不对。你用相对路径demo.db连接,真正生成的很可能不是你预期的那个文件。建议先打印一下当前工作目录,再把数据库文件的绝对路径写进连接参数,或者把路径转成标准写法。如果你连的是MySQL或PostgreSQL,就要检查主机地址、端口、用户名、密码,一层层排除。端口容易记混,MySQL常用3306,PostgreSQL常用5432,不要在代码里写成别的高位端口,当然有人会问为什么连不上,多半就是端口写错了。

5.2 中文乱码、查出来是元组、写进去再读出来变了样

数据库里的中文乱码,在SQLite里并不常见,因为SQLite本身就是UTF-8存储。多数乱码是终端显示问题,Windows上老版本终端输出中文字符会变花,解决办法是把代码文件保存为UTF-8,终端也用UTF-8,或者在输出前统一encode转码。MySQL里如果建表时没指定字符集,则会出现latin1之类的老默认值,需要建表语句里显式加CHARACTER SET utf8mb4,顺便把连接字符串里的charset也指定成utf8mb4。

“查出来是元组”这个问题我已经提过一次,解决方式是给连接设置row_factory。设置之后如果发现字段名对不上,注意你的SQL有没有给列起别名,用别名访问即可。

5.3 比语法更坑的:忘了提交、误删数据、没有备份

一句话总结,SQL语法错误都很容易靠报错信息排查,真正麻烦的是逻辑错误。比如忘了commit(),数据没写进去;比如DELETE语句忘了加WHERE,整张表被清空;比如更新语句的条件写得过宽,把不该改的行一并改了。我建议所有写操作都在事务控制下进行,危险的删除、更新先提前做一次查询,把要影响的行数看一遍再执行SQL。

实操中还有一个非常实用的习惯:操作前备份。SQLite数据库就是一个文件,复制一份到备份目录成本极低;MySQL、PostgreSQL也有各自的备份命令,虽然项目初期用不上,但知道有这回事,等正式接手业务时就不慌。

5.4 性能上容易忽略的小问题

查询慢不一定是SQL写得花哨的问题,很可能是没有索引。假如借阅记录表数据量到了几万行,而每次查询都用WHERE borrower = '阿明',数据库需要逐行扫描,效率就会下降。这时可以在borrower字段上建索引:

CREATE INDEX idx_borrower ON borrow_records(borrower);

索引的原理类似于书的目录,能大幅减少扫描量。但索引不是越多越好,写数据时要额外维护索引,所以只在频繁查询的字段上建就够了。初学者的错误是给每个字段都加索引,结果写入数据越来越慢,读也不见得多快。

6. 什么时候该上ORM和其他工具

6.1 如果你能写SQL,为什么还要ORM

有些同学会问,sqlite3原生操作其实已经挺顺手,为什么市面上还流行ORM(对象关系映射,比如SQLAlchemy)?我的判断是:小项目、学习阶段、简单查询,原生SQL完全够用,而且因为贴近数据库本身,性能也透明。但项目一旦大起来,表之间关联多、字段常变动,直接写SQL会变得很难维护。

ORM的好处是让你用Python的类和方法来操作数据表,比如定义一个类叫User,类属性对应表的字段,新增一条用户记录就相当于创建一个User对象再保存入库。程序员不用再记那么多SQL细节,跨数据库切换也更轻松。缺点是写复杂查询时,ORM可能会生成不够优化的SQL,而且调试链路会变长。

6.2 我给学习者建议的最小工具升级路线

如果你是从day39这个阶段过来的,我建议不要急着上ORM。先把原生SQLite的操作练到封装的水平,至少能做到“每个查询函数写出来,不看资料就能讲清楚连接在哪关闭”。等遇到以下情形再考虑上SQLAlchemy:项目包含十张以上互相关联的表;代码里大量字符串拼接SQL,维护开始“手痛”;团队里多人协作,需要统一数据访问方式。

还有一个折中的选择,SQLAlchemy Core,它介于“写纯SQL”和“用ORM”之间,不必定义类模型也能链式构造查询。如果你不喜欢ORM那种魔改式写法,又想让代码更结构化,可以考虑从Core入手。

另外,日常做数据分析时,用pandas.read_sql_query()可以把查询结果直接变成DataFrame,这在处理数据分析任务时非常省力。示例:

import pandas as pd import sqlite3 conn = sqlite3.connect("library.db") df = pd.read_sql_query("SELECT * FROM borrow_records", conn) print(df.head()) conn.close()

想让代码优雅一点,可以把数据库路径配置成环境变量,用os.getenv("DB_PATH", "library.db")来读取,这样换环境时不用改代码。

7. 最后再分享一点个人体会

写到这里,我真心建议你把示例代码自己敲一遍。敲代码和看代码是两种完全不同的体验,SQL的引号、占位符、提交时机,这些细节不亲手踩一次,印象就不深。我见过太多人收藏了一堆教程,但到真正写登录注册模块时,连conn.row_factory为什么设置都不知道。

真正让我把SQL和Python数据库操作“学到脑子里”的,是一次练习课上的小需求:我用Python爬了一些公开数据,把它们写进SQLite,再用SQL统计各类别数量,最后按条件筛选导出一份Excel。整个过程不过一百多行代码,但把连接、插入、查询、分组统计、关闭连接全部串起来了。很多年后接手正式项目,数据库从SQLite换成别的产品,我依然是从那套标准流程里迁移过来的,没有重新学。

如果你按照这一天的节奏学完,后续扩展方向其实很多:可以在Flask或FastAPI里写接口,把查询结果返回给前端;可以把SQLite连接换成MySQL驱动,接入真实项目;还可以爬虫程序里做个增量去重,判断某个链接是否已经爬过,这时前面学的查询API就派上用场了。反正先别急着追求花哨的框架,把上一次连接、执行一次查询、拿到结果这条路走通,后面就是水到渠成的事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询