尧图精选

30字心法吃透SQL:核心数据库操作速成指南

🕒 发布时间:2026/9/11 3:28:28 📁 来源:尧图网络
SQL这门语言入门难度一直被人高估了。很多新手看到几十行的嵌套查询、窗口函数、多表JOIN第一反应就是“这东西没个半年学不会”。但实际上日常开发里80%以上的数据库操作翻来覆去用的就那十几个关键词。标题里说“30字精通数据库操作”有点夸张但背后的道理是真的——你把核心那批关键词吃透剩下的都是组合和套模板。这篇文章适合完全没接触过数据库的小白也适合那种“会写SELECT但一优化就懵”的半吊子。我会把我这些年用SQL踩过的坑、总结出来的套路全部拆开讲不整虚的全是能直接上手的东西。你可以照着文章里的例子走一遍跑通之后基本就能应付绝大多数工作场景了。1. 30字心法把数据库操作装进一张卡片里1.1 四条核心语句覆盖90%日常操作先背下这四条其余的都是它们的变体SELECT查INSERT增UPDATE改DELETE删这四个词就是数据库操作的“四大金刚”。你回想一下你用到数据库的场景不管系统多复杂落到最底层无非就是这四件事。前端用户点了个按钮后端翻译成SQL要么是查出来给用户看要么是写一条新数据要么是改旧数据要么是把数据干掉。它们对应的基础语法也就那么几行SELECT 列名 FROM 表名 WHERE 条件; INSERT INTO 表名(列1, 列2) VALUES(值1, 值2); UPDATE 表名 SET 列1 新值 WHERE 条件; DELETE FROM 表名 WHERE 条件;注意看除了INSERT之外另外三条后面都带了WHERE。这个WHERE就是我反复要强调的重点。很多新手刚学UPDATE和DELETE的时候容易手滑把条件漏了结果就是全表的数据全被改了或者全删了。这不是段子是每个生产事故清单里排名前三的经典操作。1.2 为什么“查询”是SQL的灵魂四大操作里SELECT的使用频率能占到九成以上。这话不夸张。你写一个报表、做一个后台管理页面、接一个数据大屏核心都是查。增删改只是前期的数据准备真正长期跑在线上、被调用最频繁的永远是查询语句。所以学习SQL的正确顺序应该是先把SELECT玩透再碰INSERT、UPDATE、DELETE。SELECT本身也不是一个孤立的关键词它后面通常跟着一串“零件”SELECT FROM WHERE GROUP BY HAVING ORDER BY LIMIT。把这七个零件组装好你已经能写出非常体面的查询了。用一句生活化的话总结查询的思路FROM是进哪个仓库WHERE是筛掉哪些货GROUP BY是分堆HAVING是筛选分完堆之后的堆ORDER BY是排序LIMIT是只要前几堆。1.3 先理解SQL的执行顺序再写SQL这个知识点是区分“会写SQL”和“懂SQL”的分水岭。很多新手照着网上抄了一个SQL跑通了但稍微变一下需求就懵就是因为没搞懂SQL内部到底先执行哪一步。SQL的书写顺序是这样的SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY → LIMIT但数据库真实的执行顺序是FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT这俩顺序不一样带来的后果非常直接。最经典的坑就是你不能在WHERE里用SELECT后面才取的别名。-- 这样写会报错 SELECT name, price * quantity AS total FROM orders WHERE total 100;原因很简单执行WHERE的时候SELECT还没跑total这个别名还不存在。你得老老实实把条件写全SELECT name, price * quantity AS total FROM orders WHERE price * quantity 100;理解执行顺序之后你再看网上一堆“为什么我的别名用不了”“为什么HAVING能用的条件WHERE不能用”这类问题心里基本就有数了。WHERE是给原始行做筛选的HAVING是给GROUP BY分完的组做筛选的WHERE不能接聚合函数SUM、COUNT、AVG这些HAVING可以根源就在于两者的执行时机不同。2. 核心操作拆解每条语句的语法与细节2.1 SELECT、FROM、WHERE查询三板斧SELECT后面跟什么决定了你拿到的数据长什么样。跟*表示所有列跟具体列名就是只取那几列。这里我特别建议新手少用SELECT *尤其是线上的生产环境原因有两个一是多余的列会增加网络传输的开销二是当表结构发生变化比如别人加了一个很大的字段你的查询结果集就会莫名其妙变大程序可能直接卡死。只在临时库里随便看看数据用*无妨正式代码里我习惯把所有列名写清楚。FROM后面跟的是数据来源。大部分时候就是一张表但做复杂查询时它会变成多个表的JOIN结果甚至是一个子查询后面讲。WHERE是查询里最核心的条件过滤器。它支持的操作符我列个常用清单、!、等于、不等于、、、大小比较LIKE模糊匹配配合%和_使用IN在一个集合里相当于多个的简写BETWEEN ... AND ...在某个区间内IS NULL判断为空举个实际例子。假设有一张用户表users你想找出所有注册时间在2024年1月之后、邮箱是QQ邮箱、余额大于100块的用户SELECT name, email, balance FROM users WHERE created_at 2024-01-01 AND email LIKE %qq.com AND balance 100;这段SQL的逻辑很简单但已经用了等于、模糊匹配、比较、逻辑与。日常查询就是这么拼出来的。补充一个实战经验判断字段为空一定要用IS NULL不要用 NULL。这是一个几乎每个新手都会掉进去的坑。如果你用WHERE name NULL永远查不到数据因为NULL不等于任何东西包括NULL本身。真要判断“不是空”就写IS NOT NULL。2.2 GROUP BY、HAVING、ORDER BY分组、筛选和排序GROUP BY的意思是把数据按某一列或者多列的值分组然后配合聚合函数做统计。什么是聚合函数就是COUNT计数、SUM求和、AVG平均、MAX最大、MIN最小这五个。最常见的需求统计每天有多少笔订单。SQL是这样的SELECT DATE(created_at) AS day, COUNT(*) AS order_count FROM orders GROUP BY DATE(created_at);GROUP BY后面跟的是什么SELECT后面就只能放跟它一样的分组列以及聚合函数。不能放其他普通列这是很多新手犯的错误。比如上面这条SQL如果你想顺便查出订单表的user_note字段就会报错或者得到不确定的值因为分组之后每个组里可能有很多条不同的user_note数据库根本不知道该给你哪一条。HAVING是在分组之后再做一层筛选。前面说了WHERE不能接聚合函数那“订单数超过100的日期”这个需求就只能靠HAVINGSELECT DATE(created_at) AS day, COUNT(*) AS order_count FROM orders GROUP BY DATE(created_at) HAVING COUNT(*) 100;ORDER BY就简单了排序。默认升序ASC要倒序就写DESC。多列排序时先按第一个字段排第一个字段一样再看第二个字段SELECT name, score FROM students ORDER BY score DESC, name ASC;注意分组和排序一个很容易忽略的细节如果数据量大ORDER BY放在最后执行它需要把前面查出来的结果全部放进内存排序。如果结果集几百万行排序的时间会非常可观。这也是后面慢查询优化里的重点方向。2.3 INSERT、UPDATE、DELETE写操作的三条铁律INSERT三种写法我按实用频率排个序单行插入INSERT INTO users(name, email) VALUES(张三, zhangsanexample.com);多行插入一次插多条效率高很多INSERT INTO users(name, email) VALUES (张三, zhangsanexample.com), (李四, lisiexample.com), (王五, wangwuexample.com);从别的表查出来再插入INSERT INTO vip_users(name, email) SELECT name, email FROM users WHERE level 5;UPDATE的语法也很简单但要注意的点特别多。最标准的安全写法是先写WHERE条件再回去补SET和表名。这不是玩笑是一种防呆策略。因为WHERE是限制范围的先确定范围你的UPDATE才不会变成“全表更新”。UPDATE users SET balance balance - 50 WHERE id 42;如果你确实要更新所有行那也请你先SELECT COUNT(*)看看表里有多少数据再决定要不要真这么干。同理DELETE命令的杀伤力更大它一旦执行很难恢复。很多团队现在都有一条不成文的规定上线DELETE语句必须经过DBA审批核心业务表宁可做逻辑删除加一个is_deleted字段也不用物理删除。这个思路你在实际开发中可以直接借鉴。2.4 JOIN、LIMIT、DISTINCT高频组合技JOIN是把多张表串起来的桥梁。数据库关系型设计的本质就是把同一个实体的属性拆到多张表里避免重复存储查的时候再通过关联字段拼起来。最常用的就是INNER JOIN内连接和LEFT JOIN左连接两种。内连接取的是两个表的交集左连接则是左表全保留右边匹配不上就用NULL填充。什么场景用哪个看业务需求。比如查所有有订单的用户信息用INNER JOIN没下过单的用户不显示但查所有用户及其最近订单就算没下过单的用户也得显示在列表里那就得用LEFT JOIN。SELECT u.name, o.order_no FROM users u LEFT JOIN orders o ON u.id o.user_id;这里的u和o是表别名起了之后就让SQL清爽很多。JOIN的核心是ON后面的关联条件很多人一开始分不清ON和WHERE的区别。记住一句话ON是在JOIN的时候决定怎么拼WHERE是在拼完结果之后做过滤。LIMIT用来限制返回行数分页就靠它。简单的场景SELECT * FROM products ORDER BY id LIMIT 10 OFFSET 20;OFFSET表示跳过多少行上面这句就是取出第21到第30条。但这里有个隐患OFFSET越大查询越慢因为数据库要先数过前面所有行。业界管这个叫“深分页问题”后面我会讲优化方案。DISTINCT是去重。比如查一共有多少个不同城市SELECT DISTINCT city FROM users;它跟GROUP BY去重的区别在于DISTINCT返回的是去重后的明细行GROUP BY重点是聚合统计。如果你的目标只是“看看有哪些值”DISTINCT足够如果想算每个值的数量就必须上GROUP BY。3. 从入门到能用一套完整的实操演练3.1 先建库建表数据类型与约束纸上谈兵结束接下来我们动手。我以MySQL为例其他数据库SQL Server、Oracle、PostgreSQL语法大同小异核心思想完全一样。第一步建一张用户表。注意表结构设计时要选对数据类型这个决定了后面你能存什么、查询效率高不高、会不会浪费空间。CREATE TABLE users ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, email VARCHAR(100) NOT NULL UNIQUE, balance DECIMAL(10,2) DEFAULT 0.00, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );几个关键点说一下INT UNSIGNED无符号整数能用正数就别浪费一个符号位能装的数据范围翻倍。AUTO_INCREMENT自增主键每插一条自动加1保证唯一性。VARCHAR(50)可变长字符串50是最大长度。不要一上来就设个大数值这会影响索引效率。DECIMAL(10,2)银行余额这种精确小数必须用它不用FLOAT或DOUBLE因为浮点会有精度丢失。NOT NULL这个字段不能为空业务上必填的字段就要加。UNIQUE唯一约束邮箱不能重复注册。再建一张订单表顺便演示外键和联合主键的概念CREATE TABLE orders ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id INT UNSIGNED NOT NULL, order_no VARCHAR(32) NOT NULL, amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_id (user_id), INDEX idx_order_no (order_no) );注意我加了两个普通索引INDEX。索引是数据库里提升查询速度最核心的工具原理有点像书的目录。没索引的时候查一条WHERE user_id 100得整张表从头扫到尾有索引之后数据库先查目录定位到位置速度是数量级的提升。3.2 造数据从INSERT到事务表建好了接下来造点测试数据。先插三个用户INSERT INTO users(name, email, balance) VALUES (张三, zhangsantest.com, 200.00), (李四, lisitest.com, 150.00), (王五, wangwutest.com, 0.00);再插几笔订单INSERT INTO orders(user_id, order_no, amount, status) VALUES (1, NO20240001, 88.00, 1), (1, NO20240002, 120.00, 1), (2, NO20240003, 55.00, 0), (2, NO20240004, 200.50, 1), (3, NO20240005, 30.00, 0);这里是真实开发中最容易出问题的地方如果用户在下一笔订单时系统既要往订单表插数据又要扣减用户的余额这两件事必须同时成功或者同时失败否则数据就错了。这种“多条SQL要么全成、要么全败”的机制叫事务。START TRANSACTION; INSERT INTO orders(user_id, order_no, amount, status) VALUES(1, NO20240006, 66.00, 1); UPDATE users SET balance balance - 66.00 WHERE id 1; COMMIT;如果其中任何一条出错了就执行ROLLBACK回滚所有变更全部撤销。事务的这个特性简称为ACID关系型数据库之所以可靠这个特性功不可没。新手不用把术语背得多溜但你得养成习惯多步写操作一定要考虑是否需要事务。3.3 实战查询从单表到多表JOIN现在开始写正经的报表查询。需求统计每个用户的订单总金额只要金额超过100的按金额从高到低排。先单表完成SELECT user_id, SUM(amount) AS total_amount FROM orders WHERE status 1 GROUP BY user_id HAVING total_amount 100 ORDER BY total_amount DESC;这里面每一步的执行顺序是先把状态为1的订单筛出来然后按用户分组算出每个人的总金额再用HAVING过滤掉总金额不超过100的最后排序。但是这条SQL只返回user_id你想在报表里看到用户名字那就得把users表JOIN进来SELECT u.name, SUM(o.amount) AS total_amount FROM orders o JOIN users u ON o.user_id u.id WHERE o.status 1 GROUP BY u.id, u.name HAVING total_amount 100 ORDER BY total_amount DESC;这里要注意GROUP BY从u.id到u.name都要列出来因为只按id分组的话name就变成“非分组列”了严格模式下会报错。3.4 进阶用法窗口函数和WITH复杂查询的救星这个部分属于“入门之后马上就能吃到的进阶红利”因为窗口函数最近几年已经是高频面试题和真实业务需求了。窗口函数解决的核心问题是“组内排名”或者“组内汇总但不折叠行”。举个例子我想找出每个用户最近的一笔订单。用传统分组做你得先GROUP BY拿到每个用户的最大时间再自连回订单表又长又绕。用窗口函数的话SELECT name, order_no, amount FROM ( SELECT u.name, o.order_no, o.amount, ROW_NUMBER() OVER (PARTITION BY u.id ORDER BY o.created_at DESC) AS rn FROM orders o JOIN users u ON o.user_id u.id ) t WHERE t.rn 1;ROW_NUMBER() OVER (PARTITION BY u.id ORDER BY o.created_at DESC)的意思是按用户分组PARTITION BY组内按订单时间倒序排给每一行标一个序号最新订单的序号就是1。外层再过滤rn 1完美拿到每人最新一笔。另外一个特别实用的语法是WITH它能把一个大查询拆成一段一段的临时表可读性提升巨大。同样是上面这个需求用WITH写就是WITH ranked_orders AS ( SELECT o.user_id, o.order_no, o.amount, ROW_NUMBER() OVER (PARTITION BY o.user_id ORDER BY o.created_at DESC) AS rn FROM orders o ) SELECT u.name, r.order_no, r.amount FROM ranked_orders r JOIN users u ON r.user_id u.id WHERE r.rn 1;是不是清爽多了这种写法对新手特别友好因为你可以把复杂问题拆成步骤每个步骤一个临时查询结果最后再组合。面试官看到你写WITH第一印象都会觉得你懂“结构化查询”的精髓。4. 数据库“翻车”现场常见问题与排查技巧4.1 慢SQL优化EXPLAIN到底要看啥线上系统跑着跑着变卡了DBA让你看一下慢查询日志里面全是拖垮DB的SQL。这时候你打开EXPLAIN如果不知道看哪几列那等于白看。EXPLAIN SELECT * FROM orders WHERE user_id 42;你至少要看四样东西type访问类型从上到下性能从好到差大概是consteq_refrefrangeindexALL。看到ALL就要警惕这是全表扫描数据量大一定慢。看到index也很危险它是全索引扫描虽然比全表好一点但也不是最优。key实际用到的索引名。如果为NULL说明这条SQL没有命中任何索引。rows预估扫描的行数。这个数字越大说明SQL要干的工作量越大尽量缩小它。Extra这里出现Using filesort使用了文件排序或Using temporary使用了临时表都是在提醒你排序和分组可能很耗时值得重点关注。优化的第一板斧永远是建索引。但索引不是乱建的记住核心原则WHERE后面的条件、JOIN的关联字段、ORDER BY的排序字段才是放索引的候选位置。频繁更新的列不适合建索引因为每次写都要连带更新索引反而拖慢写入。一个表索引不要贪多三五个以内是常见范围。4.2 深分页、SELECT *、字符集编码三个容易忽略的坑深分页问题在4.4里提过这里给出标准解法。假设你要取第1000000行之后的10条数据-- 慢因为OFFSET要跳过前面100万行 SELECT * FROM orders ORDER BY id LIMIT 10 OFFSET 1000000; -- 快先在索引上定位再回表取详情 SELECT * FROM orders WHERE id ( SELECT id FROM orders ORDER BY id LIMIT 1 OFFSET 1000000 ) ORDER BY id LIMIT 10;第二种写法子查询只查了索引里的ID速度极快然后再用ID带出后面的数据。这种优化方式在订单表、日志表这种行数上千万的表里效果立竿见影。再一个容易踩的坑是乱码。建表的时候字符集一定要用utf8mb4而不是utf8因为MySQL的utf8其实不是真正的完整UTF-8它连emoji都存不了。你要是建表建成了utf8后面存表情符号就会报错或者变成问号。4.3 SQL注入是怎么回事怎么防SQL注入是数据库安全里最经典的问题。核心原因特别简单开发者把用户的输入直接拼接进了SQL字符串。比如登录功能前端传用户名和密码后端直接拼SELECT * FROM users WHERE username 用户输入 AND password 用户输入如果用户在用户名框里输入 OR 11拼出来的SQL就变成了SELECT * FROM users WHERE username OR 11 AND password xxx11永远为真登录就被绕过了。这不是理论是真实发生过的经典攻击手法。防的办法也很简单粗暴永远不要用字符串拼接SQL要用参数化查询或者预编译语句。以Java的JDBC为例String sql SELECT * FROM users WHERE username ? AND password ?; PreparedStatement pstmt conn.prepareStatement(sql); pstmt.setString(1, username); pstmt.setString(2, password);?是占位符数据库会把用户输入当作纯粹的数据来对待而不是SQL代码的一部分。即使输入里带了 OR 11它也只是个普通字符串。这个原则适用于所有编程语言和所有数据库客户端。不只是登录和查询INSERT、UPDATE、DELETE同样严禁用字符串拼接。4.4 常见问题的速查与心得平时大家在连接数据库、跑SQL的时候遇到的报错其实高度集中。我把这十几年碰到的高频问题整理成一张表方便你直接对号入座。现象排查思路连接数据库报“用户名或密码错误”检查账号是否有远程访问权限很多数据库默认只允许localhost连中文乱码先看建表字符集是否为utf8mb4再看连接串是否指定了characterEncoding导出的SQL文件导入失败检查文件里是否有触发器、存储过程等特殊对象分开执行逐个排查提示“字段不存在”但明明有确认字段名有没有引号包裹检查是否大小写敏感DELETE或UPDATE执行很慢先看WHERE条件是否走索引条件字段加了函数会导致索引失效多表JOIN结果比预期多很多大概率是关联条件不唯一ON字段在右表有多条匹配导致笛卡尔积膨胀关于工具这边也补一句我自己的习惯是轻量级看数据用HeidiSQL或者DBeaver命令行排查用系统自带的CLI一键导出整个库再导入到本地环境做复现时用mysqldump最稳。工具不在多顺手就行。最后说点我的真心话我带过不少新人也帮不少人排查过线上事故最深的体会是SQL学得好不好跟背了多少语法关系不大关键在于你实际跑了多少遍踩了多少坑。你第一次UPDATE忘记加WHERE把表改崩了这种记忆比看十遍教程都深。所以我特别建议你把这篇文章里的例子一句一句敲进本地数据库跑一遍然后自己改条件、改排序、加聚合函数看看结果有什么变化。报错了不要紧报错信息就是最好的老师。等你把增删改查、JOIN、GROUP BY、排序分页这几样玩顺了窗口函数和WITH这类进阶功能自然就水到渠成。毕竟数据库这东西谁上手早谁的经验就值钱。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →