尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

数据库DML核心操作全解析:从增删改查原理到SQL性能优化实战

数据库DML核心操作全解析:从增删改查原理到SQL性能优化实战 1. 从“增删改查”说起为什么DML是数据世界的核心操作如果你用过任何数据库哪怕只是Excel表格那你一定干过四件事往里面加新数据、删掉不要的数据、修改已有的数据以及把数据找出来看看。这四件事就是我们今天要聊的DMLData Manipulation Language数据操纵语言的核心。它不是什么高深莫测的理论而是我们每天和数据打交道时手里最趁手的“工具包”。简单来说DML就是用来和数据库里的数据“对话”的语言。它不负责创建桌子表结构或者规定谁可以坐权限那是DDL数据定义语言和DCL数据控制语言的活儿。DML只关心一件事桌子上的菜数据怎么摆、怎么换、怎么吃。在关系型数据库的世界里无论是老牌的MySQL、Oracle还是近年来备受关注的PostgreSQL也就是热词里的pgsql甚至是像SQLite这样轻量级的选手DML的语法都大同小异核心思想一脉相承。掌握DML就等于拿到了操作数据库数据的万能钥匙。为什么它如此重要因为几乎所有的应用程序其业务逻辑最终都会落地为对数据的“增、删、改、查”。用户注册是一条INSERT发布评论又是一条INSERT加上对文章评论数的UPDATE删除过期的日志是DELETE而你打开这篇文章列表背后就是一条复杂的SELECT查询。理解DML不仅能让你写出正确的SQL更能让你理解数据是如何在系统中流动和变化的这是后端开发、数据分析、运维等岗位的必备基础。2. DML命令全景图四大金刚与它们的十八般武艺DML家族主要有四位成员SELECT,INSERT,UPDATE,DELETE。别小看这四条命令它们组合起来能应对几乎所有的数据操作场景。下面我们逐一拆解看看它们到底怎么用以及背后有哪些需要注意的“坑”。2.1 SELECT数据世界的探照灯SELECT语句用于从数据库表中检索数据这是使用频率最高的DML命令没有之一。它的基础结构是SELECT 列名 FROM 表名 WHERE 条件。基础用法与核心子句选择列你可以用*选择所有列但生产中强烈建议明确指定需要的列名。这不仅能减少网络传输的数据量还能避免因表结构变更如增删列导致应用程序出错。-- 不推荐 SELECT * FROM users; -- 推荐 SELECT id, username, email FROM users;WHERE子句这是SELECT的灵魂用于过滤行。掌握各种运算符,!,,,BETWEEN,IN,LIKE和逻辑运算符AND,OR,NOT的组合是关键。SELECT * FROM orders WHERE status SHIPPED AND total_amount 100;ORDER BY子句对结果进行排序。ASC升序默认DESC降序。排序是资源消耗较大的操作尤其在数据量大且没有合适索引时。SELECT product_name, price FROM products ORDER BY price DESC, product_name ASC;LIMIT / OFFSET子句用于分页。LIMIT指定返回的行数OFFSET指定跳过的行数。这是实现“下一页”功能的基础。-- 获取第6到第15条记录每页10条的第二页 SELECT * FROM articles ORDER BY publish_time DESC LIMIT 10 OFFSET 5;进阶与性能考量连接查询JOINSELECT真正的威力在于连接多个表。INNER JOIN内连接、LEFT JOIN左连接是最常用的。理解它们区别的关键是明确“驱动表”和“匹配条件”。SELECT u.username, o.order_no, o.amount FROM users u INNER JOIN orders o ON u.id o.user_id WHERE u.country CN;注意多表连接时务必为关联字段如u.id o.user_id建立索引否则性能会呈指数级下降。同时避免连接超过3-4个表复杂的多表连接应考虑是否可以通过业务拆分或冗余字段来优化。聚合函数与GROUP BY用于数据统计如COUNT(),SUM(),AVG(),MAX(),MIN()。配合GROUP BY子句可以对数据分组统计。SELECT department_id, COUNT(*) as emp_count, AVG(salary) as avg_salary FROM employees GROUP BY department_id HAVING avg_salary 5000; -- HAVING用于过滤分组后的结果实操心得WHERE和HAVING容易混淆。记住一个简单的原则WHERE在分组前过滤行它不能使用聚合函数HAVING在分组后过滤组它可以使用聚合函数。2.2 INSERT为数据库注入新生命INSERT语句用于向表中插入新的行。看似简单但细节决定成败。基础语法与多值插入-- 插入单行明确指定列推荐 INSERT INTO table_name (column1, column2, column3) VALUES (value1, value2, value3); -- 插入多行效率更高 INSERT INTO table_name (column1, column2) VALUES (value1a, value2a), (value1b, value2b), (value1c, value2c);插入冲突处理UPSERT这是非常实用的高级特性。当插入的数据与表中现有主键或唯一约束冲突时不同数据库有不同的语法来处理。PostgreSQL / SQLite 的ON CONFLICT-- 假设id是主键冲突时更新name和updated_at字段 INSERT INTO users (id, name, email) VALUES (1, Alice, aliceexample.com) ON CONFLICT (id) DO UPDATE SET name EXCLUDED.name, updated_at NOW(); -- 冲突时什么都不做忽略插入 INSERT INTO users (id, name) VALUES (1, Bob) ON CONFLICT (id) DO NOTHING;MySQL 的ON DUPLICATE KEY UPDATEINSERT INTO users (id, name, email) VALUES (1, Alice, aliceexample.com) ON DUPLICATE KEY UPDATE name VALUES(name), email VALUES(email), updated_at NOW();踩坑记录大量数据插入时务必使用多值插入或数据库特有的批量导入工具如MySQL的LOAD DATA INFILEPostgreSQL的COPY。一条一条地INSERT会在网络通信和事务日志上产生巨大开销速度可能相差百倍。2.3 UPDATE精准的数据手术刀UPDATE用于修改表中已有的数据。它的危险性仅次于DELETE一条没有WHERE条件的UPDATE语句足以毁掉整个表的数据。安全第一永远带上WHERE子句-- 这是灾难 UPDATE users SET status inactive; -- 正确的做法精确指定要更新的行 UPDATE users SET status inactive WHERE last_login_date 2023-01-01;基于子查询的更新有时需要根据另一个表的数据来更新当前表。-- 将超过额度用户的账户状态标记为‘冻结’ UPDATE accounts a SET status FROZEN FROM (SELECT account_id, SUM(amount) as total FROM orders GROUP BY account_id) o WHERE a.id o.account_id AND o.total a.credit_limit; -- 注意不同数据库的语法略有不同上述为PostgreSQL风格。在MySQL中你可能需要使用JOIN。更新时的锁机制UPDATE操作会对涉及的行甚至更大的范围加锁阻塞其他事务的写入有时包括读取。在更新大量数据时最好分批次进行例如每次更新1000条并在业务低峰期执行以避免长时间锁表影响线上服务。-- 分批更新示例伪逻辑具体语法依数据库而定 WHILE (存在待更新记录) LOOP UPDATE large_table SET flag processed WHERE flag pending AND id IN ( SELECT id FROM large_table WHERE flag pending LIMIT 1000 ); COMMIT; -- 每批提交一次减少锁持有时间 -- 可以适当暂停如 PERFORM pg_sleep(0.1); END LOOP;2.4 DELETE谨慎使用的数据橡皮擦DELETE语句用于从表中删除行。这是一项不可逆的操作除非有备份或启用回收站功能。基础与清空-- 删除特定行 DELETE FROM logs WHERE created_at 2022-01-01; -- 清空整个表危险 DELETE FROM temp_table; -- 更快地清空整个表重置自增ID不可回滚 TRUNCATE TABLE temp_table;DELETEvsTRUNCATEDELETE是DML操作。逐行删除会在事务日志中记录每一行的删除动作因此速度较慢但可以回滚在事务内。可以带WHERE条件。TRUNCATE是DDL操作。直接释放表的数据页不记录单个行删除日志因此速度极快。无法回滚在某些数据库如PostgreSQL中它在事务中执行可以回滚但行为与DELETE不同。会重置表的自增序列。删除关联数据删除主表记录时如果有外键约束数据库会阻止删除或级联删除子表记录。这需要在设计表结构时就规划好。-- 假设orders表有外键user_id引用users.id并设置了ON DELETE CASCADE DELETE FROM users WHERE id 123; -- 这将同时删除该用户的所有订单血泪教训在执行任何DELETE操作前尤其是生产环境务必先写成SELECT语句验证条件。例如你想删除id100的记录先运行SELECT * FROM table WHERE id 100;确认这确实是你想删的那条。或者更稳妥的做法是先使用“软删除”UPDATE设置一个is_deleted标志位定期再由后台任务物理删除。3. 深入原理事务与锁——DML操作的护航者单独执行DML命令不难但要让它们在并发、高可用的系统中正确工作就必须理解两个核心概念事务和锁。3.1 事务Transaction保证操作的原子性事务是一组不可分割的DML操作序列它必须满足ACID特性原子性Atomicity事务内的所有操作要么全部成功要么全部失败回滚。一致性Consistency事务使数据库从一个一致状态转变到另一个一致状态。隔离性Isolation并发事务之间互不干扰。持久性Durability事务一旦提交其结果就是永久性的。事务的基本控制语句BEGIN; -- 或 START TRANSACTION; -- 一系列DML操作... UPDATE accounts SET balance balance - 100 WHERE id 1; UPDATE accounts SET balance balance 100 WHERE id 2; -- 如果一切正常 COMMIT; -- 如果发生错误 ROLLBACK;一个经典案例银行转账。从A账户扣钱和向B账户加钱必须在同一个事务中。如果扣钱成功但加钱失败整个事务必须回滚否则钱就“消失”了。3.2 锁Lock管理并发访问的交警当多个事务同时操作同一数据时锁机制防止数据出现不一致。DML操作会自动加锁SELECT ... FOR UPDATE这是SELECT语句中一个重要的DML相关子句。它会对查出的行加上排他锁其他事务无法修改这些行直到当前事务结束。常用于“先查后改”的并发场景如库存扣减。BEGIN; SELECT quantity FROM inventory WHERE product_id 10 FOR UPDATE; -- 锁定这行 -- 检查库存充足... UPDATE inventory SET quantity quantity - 1 WHERE product_id 10; COMMIT;如果不加FOR UPDATE在两个事务同时读到库存为1后都可能认为可以扣减导致超卖。隔离级别与并发问题数据库提供了不同的事务隔离级别在并发性能和数据一致性之间进行权衡读未提交Read Uncommitted可能读到“脏数据”。读已提交Read Committed大多数数据库的默认级别。解决了脏读但可能出现“不可重复读”同一事务内两次读同一行数据值不一样。可重复读Repeatable Read解决了不可重复读但可能出现“幻读”同一事务内两次执行相同查询返回的结果集行数不同。MySQL的InnoDB默认级别通过MVCC很大程度上避免了幻读。串行化Serializable最高隔离级别完全串行执行性能最差。性能调优提示高并发场景下长时间持有锁如大事务、慢SQL是死锁和性能瓶颈的主要根源。务必让事务尽可能短小精悍尽快提交或回滚以释放锁。对于复杂的更新考虑使用乐观锁通过版本号version字段来替代悲观锁SELECT ... FOR UPDATE减少锁竞争。4. 实战进阶动态DML与性能优化秘籍了解了基础命令和原理后我们来看一些更贴近实战的进阶话题。4.1 动态DML应对灵活多变的业务需求“动态DML”并非一个标准SQL术语它通常指在应用程序中根据运行时条件动态拼接SQL语句。这在构建灵活的查询过滤器、报表系统时非常常见。安全警告永远警惕SQL注入最原始的方式是字符串拼接这带来了巨大的安全风险——SQL注入攻击。# 危险绝对不要这样做 user_input ; DROP TABLE users; -- sql fSELECT * FROM products WHERE name LIKE %{user_input}% # 执行后SQL变成了SELECT * FROM products WHERE name LIKE ; DROP TABLE users; --%正确的做法使用参数化查询预编译语句所有主流编程语言和数据库驱动都支持参数化查询。它将SQL代码与数据分离从根本上杜绝注入。# Python (使用psycopg2 for PostgreSQL) import psycopg2 conn psycopg2.connect(...) cursor conn.cursor() user_input apple cursor.execute( SELECT * FROM products WHERE name LIKE %s, (% user_input %,) # 参数作为元组传入 )对于更复杂的动态条件如不定数量的过滤条件可以使用ORM框架如SQLAlchemy、Hibernate提供的查询构建器或者手动安全地构建SQL片段。# 使用SQLAlchemy Core动态构建查询 from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData from sqlalchemy.sql import select metadata MetaData() products Table(products, metadata, autoload_withengine) query select(products) filters [] if category_filter: filters.append(products.c.category category_filter) if price_min_filter: filters.append(products.c.price price_min_filter) if filters: query query.where(and_(*filters)) # 安全地组合条件4.2 性能优化让你的DML飞起来写出能执行的SQL只是第一步写出高效的SQL才是高手。索引是王道WHERE子句、JOIN条件、ORDER BY和GROUP BY的列通常是索引的候选列。使用EXPLAIN命令或EXPLAIN ANALYZE查看查询计划确认是否用上了索引。EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id 123 AND status PAID;输出会告诉你是否进行了全表扫描Seq Scan还是使用了索引扫描Index Scan。避免SELECT *重申一遍只取需要的列。特别是当表中有TEXT、BLOB等大字段时。优化JOIN顺序将数据量小的表作为驱动表放在FROM后大数据量表作为被驱动表放在JOIN后。现代数据库查询优化器通常会帮你做这件事但复杂的多表连接仍需留意。分页查询优化传统的LIMIT N OFFSET M在偏移量M很大时非常慢因为数据库需要先扫描并跳过前M行。优化方案使用“游标分页”或“基于键的分页”。-- 传统分页慢 SELECT * FROM articles ORDER BY id LIMIT 10 OFFSET 10000; -- 基于键的分页快 SELECT * FROM articles WHERE id last_seen_id ORDER BY id LIMIT 10;批量操作如前所述批量INSERT/UPDATE/DELETE远比单条操作高效。对于数万以上的数据操作考虑拆分成批次并在事务中提交。4.3 常见问题排查实录问题UPDATE或DELETE执行极慢甚至卡住。排查首先检查是否有未提交的长事务锁住了目标行。可以使用数据库的管理命令查看当前锁信息如PostgreSQL的pg_locks和pg_stat_activityMySQL的SHOW PROCESSLIST和INNODB_LOCKS。其次检查WHERE条件是否没有用到索引导致全表扫描并锁定大量行。问题明明WHERE id1却影响了多行数据。排查确认id字段是否真的是主键或唯一约束。如果没有约束表中可能存在多条id1的记录。这是表结构设计缺陷应尽快修复。问题程序中出现“死锁”错误。排查死锁通常由多个事务以不同的顺序请求和持有锁造成。例如事务A锁了行1请求行2事务B锁了行2请求行1。数据库会中止其中一个事务。解决方案是1) 尽量以相同的顺序访问资源2) 使用更小粒度、更短时间的事务3) 在业务层实现重试机制。问题SELECT查询在测试环境很快在生产环境很慢。排查生产环境数据量远大于测试环境。首先用EXPLAIN对比查询计划。常见原因生产环境索引未正确创建或失效统计信息过时导致优化器选择了错误的执行计划需要ANALYZE表生产环境并发高锁等待或资源争用严重。
返回列表