MySQL 系列:第5篇 从一张表中精准取数
IT策士 10余年一线大厂经验专注 IT 思维、架构、职场进阶。我会在各个平台持续发布最新文章助你少走弯路。前面几篇我们建好了表、插入了数据。今天终于要回答那个核心问题怎么把数据高效、精准地取出来SELECT 是 SQL 中使用最频繁的语句掌握它就意味着你能随心所欲地“问”数据库任何问题。本篇用 Python 配合实战彻底拿下单表查询。1. 准备数据搭建测试环境先用 Python 创建一个employees表并灌入足够多的测试数据供后续所有查询使用。importmysql.connectorimportrandomimportstring connmysql.connector.connect(host127.0.0.1,port3306,userroot,passwordMyNewPass123!,databaseshop)cursorconn.cursor()# 建表cursor.execute( CREATE TABLE IF NOT EXISTS employees(idINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50)NOT NULL, department VARCHAR(30), salary DECIMAL(10,2), age TINYINT UNSIGNED, city VARCHAR(20), hire_date DATE, is_active TINYINT DEFAULT1)ENGINEInnoDB)# 清空旧数据cursor.execute(TRUNCATE employees)# 批量插入 20 条测试数据departments[技术部,产品部,市场部,财务部,人事部]cities[北京,上海,深圳,杭州,成都]names[张三,李四,王五,赵六,钱七,孙八,周九,吴十,郑一,冯二,陈三,褚四,卫五,蒋六,沈七,韩八,杨九,朱十,秦一,尤二]data[]foriinrange(20): namenames[i]deptrandom.choice(departments)salaryround(random.uniform(8000,30000),2)agerandom.randint(22,55)cityrandom.choice(cities)hire_datef202{random.randint(0,5)}-{random.randint(1,12):02d}-{random.randint(1,28):02d}data.append((name,dept,salary,age,city,hire_date))cursor.executemany(INSERT INTO employees (name, department, salary, age, city, hire_date) VALUES (%s,%s,%s,%s,%s,%s), data)conn.commit()print(f✅ 插入了 {cursor.rowcount} 条测试数据)预期输出2. SELECT 基础想看什么就写什么2.1 最简单的查询# 查看所有列cursor.execute(SELECT * FROM employees)rowscursor.fetchall()print(f共 {len(rows)} 条记录\n)# 只看特定列cursor.execute(SELECT name, department, salary FROM employees)forrowincursor.fetchmany(3):# 只取前 3 行print(row)预期输出共20条记录(张三,技术部, Decimal(18500.00))(李四,市场部, Decimal(9200.00))(王五,财务部, Decimal(27300.00))SELECT *的陷阱生产环境中尽量避免*因为返回了不需要的列浪费网络带宽表结构变更后列顺序可能变化导致代码出 bug明确列出列名代码可读性更高2.2 列别名AS让结果更易读同时可在 Python 中通过列名取值cursor.execute( SELECT name AS 姓名, department AS 部门, salary AS 月薪 FROM employees)forrowincursor.fetchmany(3): print(f{row[0]} | {row[1]} | {row[2]})3. WHERE条件筛选的艺术WHERE 是 SELECT 的灵魂——不加条件就是全表扫描加了条件才能精准命中。3.1 比较运算符# 查询月薪大于 20000 的员工cursor.execute(SELECT name, salary FROM employees WHERE salary 20000)print(高薪员工)forrowincursor.fetchall(): print(f {row[0]} - ¥{row[1]})3.2 逻辑运算符AND、OR、NOT# 技术部且月薪大于 15000cursor.execute( SELECT name, department, salary FROM employees WHERE department技术部AND salary15000)print(技术部高薪, cursor.fetchall())# 北京或上海的员工cursor.execute( SELECT name, city FROM employees WHERE city北京OR city上海)print(京沪员工, cursor.fetchall())# 不是技术部的员工cursor.execute( SELECT name, department FROM employees WHERE NOT department技术部)3.3 BETWEEN区间查询# 月薪在 10000 ~ 20000 之间cursor.execute( SELECT name, salary FROM employees WHERE salary BETWEEN10000AND20000)print(中等薪资, cursor.fetchall())# 等同于 WHERE salary 10000 AND salary 200003.4 IN列举匹配# 查询指定部门的员工cursor.execute( SELECT name, department FROM employees WHERE department IN(技术部,产品部))print(技术与产品, cursor.fetchall())IN比多个OR更简洁而且 MySQL 会对其内部优化将列表转为二分查找。3.5 LIKE模糊查询%匹配任意多个字符_匹配单个字符# 查询姓“张”的员工cursor.execute(SELECT name FROM employees WHERE name LIKE 张%)print(张姓员工, cursor.fetchall())# 查询名字第二个字是“十”的cursor.execute(SELECT name FROM employees WHERE name LIKE _十%)print(第二个字是十, cursor.fetchall())性能警告LIKE %张这种前缀模糊会导致索引失效因为 BTree 索引只能从左向右匹配。后续索引章节会详解。3.6 IS NULL / IS NOT NULL# 查询没有填写年龄的员工假设允许 NULLcursor.execute(SELECT name, age FROM employees WHERE age IS NULL)注意不能用 NULL必须用IS NULL。4. DISTINCT去重想知道公司员工分布在哪些城市cursor.execute(SELECT DISTINCT city FROM employees)print(城市分布,[row[0]forrowincursor.fetchall()])# 多列去重cursor.execute(SELECT DISTINCT department, city FROM employees)print(部门-城市组合, cursor.fetchall())5. ORDER BY排序# 按薪资降序薪资相同则按年龄升序cursor.execute( SELECT name, salary, age FROM employees ORDER BY salary DESC, age ASC)print(薪资排行榜)forrowincursor.fetchall(): print(f {row[0]} | ¥{row[1]} | {row[2]}岁)ASC升序默认DESC降序多列排序先按第一列排相同时按第二列排6. LIMIT 与 OFFSET分页查询这是构建任何列表页面都少不了的技能。6.1 基础分页# 每页 5 条取第 1 页page1page_size5offset(page -1)* page_size cursor.execute( SELECT id, name, salary FROM employees ORDER BYidLIMIT %s OFFSET %s,(page_size, offset))print(f第 {page} 页)forrowincursor.fetchall(): print(f #{row[0]} {row[1]} ¥{row[2]})6.2 快捷写法LIMIT offset, count是另一种写法但不推荐因为可读性差-- 这两种等价 LIMIT5OFFSET10LIMIT10,5-- 不推荐与 LIMIT count OFFSET offset 顺序相反6.3 分页深翻的性能问题当OFFSET很大时比如第 10000 页MySQL 仍然需要扫描前面所有的行再丢弃效率极低。解决方案后续优化篇会讲“延迟关联”等技巧。7. 实战小工具用 Python 封装一个灵活的查询函数将今天学的知识点封装成可复用的工具函数def query_employees(filtersNone,sort_byid,sort_orderASC,page1,page_size10): 灵活的 employee 查询器 filters: dict如{department:技术部,salary_min:10000} connmysql.connector.connect(host127.0.0.1,port3306,userroot,passwordMyNewPass123!,databaseshop)cursorconn.cursor(dictionaryTrue)# 返回字典格式where_clauses[11]# 占位方便动态拼接params[]iffilters:ifdepartmentinfilters: where_clauses.append(department %s)params.append(filters[department])ifcityinfilters: where_clauses.append(city %s)params.append(filters[city])ifsalary_mininfilters: where_clauses.append(salary %s)params.append(filters[salary_min])ifsalary_maxinfilters: where_clauses.append(salary %s)params.append(filters[salary_max])ifname_likeinfilters: where_clauses.append(name LIKE %s)params.append(f%{filters[name_like]}%)where_sql AND .join(where_clauses)offset(page -1)* page_size sqlf SELECT id, name, department, salary, city FROM employees WHERE{where_sql}ORDER BY{sort_by}{sort_order}LIMIT %s OFFSET %s params.extend([page_size, offset])cursor.execute(sql, params)resultscursor.fetchall()# 查询总记录数cursor.execute(fSELECT COUNT(*) AS total FROM employees WHERE {where_sql}, params[:-2])totalcursor.fetchone()[total]cursor.close()conn.close()returnresults, total# 使用示例results, totalquery_employees(filters{department:技术部,salary_min:15000},sort_bysalary,sort_orderDESC)print(f共 {total} 条本页 {len(results)} 条)forrinresults: print(f {r[name]} | {r[department]} | ¥{r[salary]})8. 动手试试探索员工数据基于我们插入的 20 条数据请完成以下挑战查询 30 岁以下且月薪超过 15000 的员工按薪资降序排列只显示前 3 条。查询在北京或深圳的市场部员工。查询名字包含“十”字的所有员工去重后按姓名排序。实现分页功能每页 4 条打印第 2 页的数据同时打印总页数。参考答案可在控制台中逐一验证预期能匹配到正确的结果集。动手提示将以上查询翻译为 Python 代码执行观察fetchall()返回的数据是否与你的预期一致。如不一致仔细检查 WHERE 条件的逻辑组合。9. 总结本篇我们完整掌握了 SELECT 单表查询的核心语句SELECT 列名代替*明确意图WHERE配合、、AND/OR、BETWEEN、IN、LIKE精准筛选DISTINCT去重ORDER BY排序LIMIT OFFSET实现分页这些是 SQL 的最高频操作也是后续 JOIN、子查询、窗口函数等高级话题的基石。下一篇我们将解锁函数与分组聚合用 SQL 直接做数据分析。下次见想了解更多还可以去各个平台搜索「IT策士」一起升级 IT 思维