1. 从零开始学SQL,先搞清楚它到底是个什么东西
先说点题外话。很多人看到SQL这三个字母就头大,觉得这是程序员才能碰的东西。但实际上,SQL(结构化查询语言)是操作关系型数据库的标准语言,它的核心逻辑就一句话:你想从表格里拿什么数据,告诉电脑怎么拿;你想往表格里加什么、改什么、删什么,告诉电脑怎么操作。
我见过不少零基础的学员,第一天接触SQL的时候被各种概念吓退,什么表、字段、记录、主键、外键、索引,听着高深莫测。但换个角度想,SQL操作的对象本质上就是一张张类似Excel的二维表格。表头叫字段,每一行数据叫记录,仅此而已。你Excel用得溜,SQL的入门门槛就比想象中低很多。
这篇文章适合谁?完全没碰过数据库的小白、刚转行做数据分析或后端开发的新人、以及用Excel处理数据到了瓶颈想升级的人。我会从最基础的环境搭建讲起,把查询、插入、更新、删除这四类核心操作全部过一遍,每一步配代码和解释,最后再分享一批我在实际工作中踩过的坑。
学SQL最大的好处是:它几乎是所有数据相关工作的地基。不管是做数据分析、做后台开发、做运营报表,还是以后想学更复杂的数据库知识,SQL都是绕不开的第一步。而且它的语法非常接近英文口语,比如“SELECT name FROM users WHERE age > 18”翻译过来就是“从用户表里选出年龄大于18岁的姓名”,上手难度远比你想象的低。
这篇文章属于实战笔记性质,建议你跟着文章边读边敲,不要干看。SQL这东西,光看不练等于白看,敲几遍比看十遍都管用。
2. 环境准备与入门第一步:建表
2.1 零基础选什么数据库工具最省心
市面上的关系型数据库很多,常见的有MySQL、PostgreSQL、SQLite、SQL Server、Oracle。对零基础来说,我强烈推荐先拿SQLite练手。原因很简单:
- 零配置:SQLite就是一个单文件数据库,不需要安装独立服务,不用配置端口、账号密码。
- 开箱即用:很多工具自带SQLite支持,省去一大堆环境问题。
- 标准语法:SQLite遵循SQL标准,你在上面练会的语法,换到MySQL或PostgreSQL基本通用,个别函数有差异,但核心增删改查完全一致。
如果你用Windows,可以下载一个开源的数据库管理工具,比如DBeaver或者SQLiteStudio。我个人更推荐DBeaver,跨平台,支持几乎所有数据库,界面直观,而且是免费开源的。装好之后再新建一个SQLite数据库文件,把表格建进去就行。
当然,如果你所在的公司或课程用的是MySQL,那也没问题,核心语法几乎一样。MySQL需要额外安装服务端,用Navicat或DBeaver连接,第一次配置稍麻烦一点,但网上教程很多。我的建议是:先别纠结用哪个数据库,赶紧把环境跑起来,把语法练熟才是正事。
2.2 CREATE TABLE建表:字段、类型、主键一个都不能少
建表是SQL操作的第一步,相当于在数据库里划出一块地方放你的数据。先看一个最简单但完整的建表语句:
CREATE TABLE users ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, age INTEGER, city TEXT DEFAULT '未知' );这条语句干了这么几件事:
CREATE TABLE users:创建一张名为users的表。- 括号内定义了4个字段(列):
id、name、age、city。 id INTEGER PRIMARY KEY:id是整型,且设为主键,也就是每条记录的唯一标识。name TEXT NOT NULL:name是文本类型,且不能为空。age INTEGER:age是整数类型。city TEXT DEFAULT '未知':city是文本类型,如果插入数据时不指定这个字段的值,会自动填上“未知”。
在真实的业务场景中,建表是一门学问,字段类型选错了后面可能要付出迁移数据的代价。比如年龄这种字段,用整数类型INTEGER就够了,有人非要用字符串类型存,结果排序的时候发现“9”排在“10”后面,因为字符串按字符顺序排。这就是类型选择不严谨导致的经典问题。
主键的概念对零基础来说也很重要。你可以把主键理解为每行记录的身份证号,它必须唯一,且不能为NULL。建表时最好显式指定主键,否则后续做数据更新和删除的时候会非常被动——因为你没办法精准定位到某一条记录。
注意:在SQLite中,如果忘记写主键,系统会隐式生成一个
rowid,但这不是你定义的业务主键。在MySQL中不指定主键也能建表,但没有主键的表在做数据更新时容易出大问题,尤其在生产环境,这叫“无主键表”,需要尽量避免。
2.3 添加测试数据:没有数据的表就是空壳子
建好表之后,我们要往里塞点数据,方便后面练查询。SQLite和MySQL在插入多行数据时的语法略有差异,我下面给出兼容性较好的写法,用单条INSERT语句插多行(SQLite从3.7.11版本开始支持,MySQL原生支持):
INSERT INTO users (id, name, age, city) VALUES (1, '张三', 25, '北京'), (2, '李四', 30, '上海'), (3, '王五', 22, '广州'), (4, '赵六', 35, '深圳'), (5, '孙七', 28, '杭州');这条语句的意思很直白:往users表里插入5行,分别指定id、name、age、city这4个字段的值。因为建表时city有默认值,所以如果某行不写city,插入后它也会自动变成“未知”。
这里有个细节新手常犯的错误:INSERT INTO users VALUES (1, '张三', 25)这样写是错的,因为建表有4个字段,你只给了3个值。你必须要么写全所有字段的值,要么显式列出字段名,否则数据库不知道你把值对应到哪个字段上。我见过太多人在这一步报错,其实多看几遍报错信息就能发现,它明确说的是“列数不匹配”。
现在你已经有了一个带数据的表,后面所有的查询、更新、删除操作都要在这张表上练。
3. 查询:SQL里最常用、也最能玩出花的部分
3.1 SELECT基础语法:字段选择与条件过滤
查询是SQL里使用频率最高的操作。它的基本结构是“SELECT哪些字段 FROM哪张表 WHERE什么条件”。先来几个最简单的例子:
-- 查询所有字段 SELECT * FROM users;-- 查询指定字段 SELECT name, age FROM users;-- 带条件查询 SELECT name, age FROM users WHERE age > 26;第一条SELECT *表示选出所有字段,日常测试方便,但实际工作中不建议在表字段很多的时候用SELECT *,因为会浪费网络带宽和查询时间,尤其在生产库上,这是需要留意的习惯。第二条只取name和age两列,这在报表场景中很常见——只取你需要的字段,不拖泥带水。第三条加上WHERE条件,相当于告诉数据库:我只想看年龄大于26的人。
WHERE后面能跟的运算符很多,除了>、<、=、>=、<=、!=这些比较运算,还有AND、OR、NOT这类逻辑组合。比如:
SELECT name FROM users WHERE city = '北京' AND age > 24;这条语句的意思是筛选出城市为北京且年龄大于24的人的名字。要注意AND和OR的优先级问题,AND的优先级高于OR。如果你的条件是“城市为北京或上海,且年龄大于24”,写的时候最好加上括号:
SELECT name FROM users WHERE (city = '北京' OR city = '上海') AND age > 24;不加括号的话,数据库会先处理AND,再把结果和另一个OR条件合并,得到的结果完全不一样。这个细节我在面试别人时经常拿出来考,踩过坑的人一抓一个准。
3.2 ORDER BY排序、LIMIT分页与去重
查询结果默认返回的顺序是不确定的,所以需要ORDER BY来指定排序规则:
-- 按年龄从大到小排序 SELECT name, age FROM users ORDER BY age DESC;-- 按年龄从小到大排序,年龄相同按姓名排序 SELECT name, age FROM users ORDER BY age ASC, name ASC;DESC表示降序,ASC表示升序。排序在报表场景很常用,比如你要看消费金额最高的前10个用户,就是“ORDER BY金额DESC加LIMIT 10”。
说到LIMIT,它的作用是限制返回的行数:
-- 查询年龄最大的3个人 SELECT name, age FROM users ORDER BY age DESC LIMIT 3;在SQLite和MySQL中,LIMIT都支持。SQL Server用TOP,Oracle用ROWNUM,但入门阶段先掌握LIMIT就够用了。
去重也是查询中很常见的一个需求,关键词是DISTINCT:
-- 查询所有不重复的城市 SELECT DISTINCT city FROM users;如果只是想知道“这张表里都有哪些城市”,用DISTINCT比SELECT *后再去代码里手动处理高效得多。但要注意,DISTINCT作用于你选择的所有字段的组合,也就是说SELECT DISTINCT city, age表示的是“城市和年龄组合起来不重复”,不是单看城市。
3.3 聚合函数与GROUP BY分组统计
聚合函数是SQL查询里最有价值的部分,它们把多行数据计算成一个结果。常用聚合函数有:
COUNT():统计行数SUM():求和AVG():求平均MAX()/MIN():取最大/最小值
举个实际场景:你要统计用户表里一共有多少人、平均年龄多少、最大年龄多少。一条查询就搞定:
SELECT COUNT(*) AS 人数, AVG(age) AS 平均年龄, MAX(age) AS 最大年龄 FROM users;这里的AS是给计算结果起个别名,让返回的列名更可读。不加AS也可以,但列名会变成COUNT(*)这种,看着别扭。
如果你需要按不同维度分组统计,就要用到GROUP BY。比如说,统计每个城市有多少人:
SELECT city, COUNT(*) AS 人数 FROM users GROUP BY city;这条语句的意思是:先把数据按city分组,再对每组执行COUNT(*)。最终返回一行为一个城市及其人数。这个逻辑我打个比方:假设你有100张照片,要按拍摄地点归类,每个地点数一下张数。GROUP BY就是分类动作,COUNT(*)就是数张数动作。
在GROUP BY分组之后想过滤分组结果,不能用WHERE,要用HAVING。区别在于:WHERE在分组前过滤原始行,HAVING在分组后过滤组。比如“查询人数超过2个的城市”:
SELECT city, COUNT(*) AS 人数 FROM users GROUP BY city HAVING COUNT(*) >= 2;这个区别很多初学者搞混,实际上记住一句话就行:WHERE管的是明细,HAVING管的是分组结果。
3.4 JOIN联表查询:从一张表到多张表
实际业务中,数据不会只存在一张表里。为了减少冗余,通常会把数据拆分到多张表中,然后用一个关联字段把它们连接起来。这就是JOIN的用武之地。
假设我们还有一张订单表orders,记录每个用户的下单情况:
CREATE TABLE orders ( id INTEGER PRIMARY KEY, user_id INTEGER, product TEXT NOT NULL, amount REAL );插入几条数据:
INSERT INTO orders (id, user_id, product, amount) VALUES (1, 1, '手机', 4000), (2, 1, '耳机', 500), (3, 2, '电脑', 8000), (4, 4, '键盘', 300);现在你想查“每个订单对应的用户名”,光看orders表不够,因为里面只有user_id,没有用户姓名。这时需要把orders表和users表连接起来,连接的条件是orders.user_id = users.id:
SELECT orders.product, orders.amount, users.name FROM orders JOIN users ON orders.user_id = users.id;这段代码的逻辑是:遍历订单表的每一行,找到user_id,然后去用户表里匹配id相同的行,最后把两个表的指定字段拼在一起返回。
JOIN有很多种:INNER JOIN(只返回两表匹配上的行)、LEFT JOIN(返回左表所有行,右表没有匹配则补NULL)、RIGHT JOIN(反之)、FULL OUTER JOIN(只要任一表有就返回)。入门阶段先掌握INNER JOIN和LEFT JOIN就够用了。
这里有个高频坑:JOIN不写ON条件会导致笛卡尔积。什么叫笛卡尔积?简单说就是左表的每一行和右表的每一行都会组合一次,产生大量无意义的数据。比如左表5行、右表4行,不写ON会直接得到20行。如果你发现查询结果莫名其妙多出很多行,第一个要检查的就是JOIN条件有没有写对。
4. 增删改:修改数据才是真正的日常操作
4.1 INSERT:插入数据的多种姿势与注意事项
很多人以为SQL入门就是“SELECT天下无敌”,实际工作中你还会频繁用到插入、更新和删除。先补全插入操作的细节。
前面我们提到过基本的INSERT写法:
INSERT INTO users (id, name, age, city) VALUES (6, '周八', 29, '成都');如果要插入多条,可以逗号分隔多个值组。还有一种常见场景:把一张表的查询结果直接插入另一张表,语法是:
INSERT INTO users (id, name, age, city) SELECT id, name, age, city FROM tmp_users;这种写法在数据迁移、备份恢复、报表生成中非常常见。核心要求是:查询结果的列数和类型要和目标表的字段匹配。比如SELECT出来5列,但你插入的目标表只有4个字段,数据库直接报错。
插入数据时还有一个易踩的坑:主键冲突。如果你给一条记录指定的id已经存在,数据库会报重复键错误。解决思路通常是让主键自动生成,而不是手动指定。很多表建表时是这样写的:
id INTEGER PRIMARY KEY AUTOINCREMENT这样插入时就不用管id了,数据库自动帮你生成唯一值:
INSERT INTO users (name, age, city) VALUES ('吴九', 26, '武汉');注意,只要表结构里声明了自增主键,插入时就千万别再显式写id字段了,否则可能触发键冲突或导致自增序列错乱。
4.2 UPDATE:更新数据,一失手成千古恨的操作
更新数据的语法是:
UPDATE users SET age = 26 WHERE name = '张三';意思很清楚:把name为“张三”的那行数据的age改成26。
这里想插一句我个人的惨痛经验。我在初期刚接触数据库的时候,有一次想更新某张表里的部分数据,写语句时忘了加WHERE:
UPDATE users SET age = 30;结果整张表所有人的年龄都变成了30。虽然当时是测试表,没造成严重后果,但这个教训我一直记到现在:**UPDATE不写WHERE,更新的是全表的数据。**这句话请一定记住,它不是开玩笑的。
所以每次写UPDATE之前,我都建议你先写一条等价的SELECT,确认你定位到的记录是正确的:
SELECT * FROM users WHERE name = '张三';确认无误后,再把它改成UPDATE语句执行。这个习惯看着多了一步,实际能帮你规避90%的误操作风险。
UPDATE还可以同时更新多个字段:
UPDATE users SET age = 27, city = '重庆' WHERE name = '李四';多个字段之间用逗号分隔,注意别把逗号写成AND,那是最常见的语法错误之一。
4.3 DELETE:删除数据和清空表要分清
删除数据的语法是:
DELETE FROM users WHERE name = '王五';这条语句会把name为“王五”的那一行删掉。同样,不写WHERE会清空整张表:
DELETE FROM users;这和UPDATE是同一个道理,危险性更大。删掉的数据在大多数数据库里是不容易恢复的,所以实际操作中,DELETE语句必须带上WHERE,而且要格外小心。
如果你真的要把整张表的数据清空,还有另一个选择:
DELETE FROM users;在MySQL里还有一个更快的:TRUNCATE TABLE users。区别在于:DELETE逐行删除,而且触发器会执行,自增主键的计数值不会重置;TRUNCATE直接快速清空,自增主键会重置回初始值。但在SQLite里没有TRUNCATE,实际使用时要区分你用的数据库类型。
另外说一个很多新手会忽略的点:DELETE删的是数据,不是表。如果你希望连表结构和定义都删掉,要用DROP TABLE users。这两者的区别,用一句话概括就是:DELETE是把房子里的家具清空,DROP是把整栋房子拆了。
4.4 事务:把多条增删改操作绑成一组
在真实业务中,一个操作往往涉及多条增删改。比如转账,A扣钱和B加钱这两个动作必须同时成功或同时失败,不允许出现A扣了钱但B没收到的情况。这时候就要用到事务。
事务的关键词是BEGIN、COMMIT和ROLLBACK:
BEGIN; UPDATE account SET balance = balance - 100 WHERE user_id = 1; UPDATE account SET balance = balance + 100 WHERE user_id = 2; COMMIT;执行流程是:BEGIN开启事务,中间多条SQL先执行但不会真正生效,只有执行COMMIT后才会统一提交。如果在执行过程中发现中间某一步出错,可以执行ROLLBACK回滚,回到事务开始前状态,所有修改都不生效。
这个机制就像是你在Word里编辑文档,先输入一堆内容,保存(COMMIT)后才落盘;如果不想要了,直接关闭不保存(ROLLBACK)。没有事务机制的话,你只能眼睁睁地看着数据改了一半,还会产生脏数据。
零基础阶段不需要深挖事务的底层原理和隔离级别,但要养成一个习惯:**凡是涉数据一致性要求的批量增删改操作,就要想到事务。**等你以后用到MySQL或者PostgreSQL,事务的很多细节会进一步展开,但基本概念现在就可以建立。
5. 避坑指南:来自真实作业现场的问题速查
5.1 关键字、大小写、注释:细节里藏着的坑
SQL关键字本身不区分大小写,但表名和字段名在有些数据库中区分大小写。稳妥起见,我建议表名和字段名统一用小写,单词之间用下划线分隔。比如user_name,而不是userName,也不是UserName。这样可以最大程度避免在不同的数据库系统中来回切换时遇到命名兼容性问题。
关于注释:SQL支持两种注释方式。--后面是单行注释,/* */是多行注释。多写注释是个好习惯,尤其是SQL语句比较长的时候。我见过很多所谓的“祖传SQL”,几百行没有一行注释,后期维护只能靠猜,真的非常痛苦。
还有一个特别容易犯的低级错误:字符串值必须用单引号。比如WHERE name = '张三',这里的“张三”必须用单引号括起来。用双引号在某些数据库(比如MySQL)也能用,但SQL标准是单引号,而且在SQLite中双引号在某些场景下会被当成字段名解析,容易出莫名其妙的错误。所以统一用单引号,别给自己找麻烦。
5.2 NULL和空字符串的区别:这俩真的不一样
很多新手会把NULL和空字符串''混为一谈,但它们完全是两回事。NULL表示“没有值”,是未知状态;空字符串表示“有值,但值是空的”,是已知状态。
用WHERE age = NULL去查数据,永远查不到任何东西。正确写法是WHERE age IS NULL。同样,判断不是空要用IS NOT NULL,而不是!= NULL。
如果你在查询中发现某些应该匹配上的数据死活匹配不到,先检查一下该字段是不是有NULL值。尤其在JOIN关联时,如果关联字段里有NULL,这些行是不会被匹配上的,这在数据统计时很容易导致“对不上账”的情况。
5.3 字符串匹配与模糊查询。LIKE和通配符
有时候你只记得用户姓名的一部分,比如知道一个用户的名字里有“三”这个字,想查出所有匹配的人。这时候可以用LIKE:
SELECT * FROM users WHERE name LIKE '%三%';%是SQL里的通配符,表示任意长度的任意字符序列。%三%的意思就是“包含‘三’这个字的任意字符串”。还有一个下划线_,表示任意单个字符。比如'_三'表示两个字符且第二个字是“三”的字符串。
需要注意的是:LIKE在数据量大时无法高效利用索引,速度会比较慢。在大数据表上频繁做LIKE '%xxx%'这种前后模糊匹配,会让查询性能急剧下降。这不是说不能用,而是要知道它的代价。
我刚接触SQLite的时候,对通配符理解得不够深入,写WHERE city LIKE '北京',直接等值判断就行却用了LIKE。虽然结果对,但性能没有区别,纯粹是多此一举,从代码可读性角度看也不太好。
5.4 条件顺序和括号问题。一个让结果跑偏的坑
前面提到了AND、OR优先级的问题,这里再展开说一次。SQL里AND的优先级高于OR,所以如果条件是你想混合使用,把OR相关的条件放在括号里是必须的。
我曾经见过一段查询代码,目标是“找出城市为北京或上海,且年龄大于25的用户”:
SELECT * FROM users WHERE city = '北京' OR city = '上海' AND age > 25;这个语句实际执行时会把条件解析成“城市为北京”或“城市为上海且年龄大于25”,结果是所有北京用户都出来了,完全违背了查询意图。正确的写法是:
SELECT * FROM users WHERE (city = '北京' OR city = '上海') AND age > 25;这个坑在笔试和面试题里经常出现,在实际写SQL时更是高频错误。我的经验是:**只要WHERE里面同时出现OR和AND,就无条件加上括号,哪怕你记得优先级规则,也最好用括号把逻辑表达清楚。**这样既防止自己搞错,也方便别人阅读。
5.5 DELETE和UPDATE忘了WHERE条件,数据库的“大事故”多由此而来
二刷这个点,因为它实在太重要了。
我的习惯是:写UPDATE和DELETE时,先把WHERE条件写完整,再回去补UPDATE或DELETE关键字。也就是倒着写语句。比如我想更新某条记录,先写:
SELECT * FROM users WHERE id = 10;确认这个id = 10就是目标记录。然后改成:
UPDATE users SET age = 30 WHERE id = 10;这样操作能最大程度避免漏写WHERE导致的全局更新或全局删除。另外,在MySQL里,如果你执行UPDATE或DELETE时开启了安全模式(默认会在工具里开启),不带WHERE直接执行会报错,这是数据库或者客户端工具在保护你。如果遇到这种报错,不要慌,不是你的SQL写错了,而是安全机制在拦你。
5.6 索引:让你的查询变快的神秘加速器
当表里的数据量变大之后,查询会变慢。这时候可以通过索引来加速。索引相当于书的目录,没有索引你要一页页翻,有索引就能直接翻到目标页。
创建索引的语法很简单:
CREATE INDEX idx_users_age ON users(age);这条语句给age字段建了一个索引。以后你执行WHERE age > 26这类查询时,数据库会优先通过索引快速定位,而不是全表扫描所有数据。
但索引不是越多越好,它有几个副作用:占用磁盘空间,并且每次插入、更新、删除数据时都要同步维护索引,拖慢写操作速度。所以只给频繁查询、区分度高的字段建索引就够了,别每个字段都加。区分度高的意思是这个字段的值分布广泛,比如姓名、手机号;区分度低的字段比如性别,就那么几个取值,建了索引收益也很有限。
5.7 备份与误删。数据安全最后一道防线
每个用过数据库的人,大概都经历过至少一次“误删数据”的惊魂时刻。我自己的做法是:在做任何批量增删改之前,先把相关的表导出一份备份。
在SQLite中,备份最简单的方式是把数据库文件复制一份,改个带时间戳的名字:
cp mydb.db mydb_20250101.bak在MySQL中,可以通过命令行工具导出:
mysqldump -u root -p mydb > mydb_backup.sql备份这件事,在个人学习阶段可能觉得麻烦,但在工作中确实是救命绳。特别是在清理脏数据、批量更新状态这种高风险操作之前,花一分钟做一个备份,能帮你省下几个小时的恢复时间。
6. 实战收尾:一份完整SQL练习的路径建议
学了语法,最重要的还是动手练。我建议你按照下面这个顺序自己走一遍,每一步都在自己的数据库上实际操作,而不是光看我写的代码:
先建一张学生表,包含学号、姓名、性别、班级、年龄、入学年份;再建一张成绩表,包含学号、科目、分数。然后依次练习:
- 查询所有学生信息。
- 查询某个班级的男生名单。
- 按年龄从大到小排序,取前5名学生。
- 统计每个班级的人数。
- 查出每门课程的最高分、最低分和平均分。
- 用
JOIN把两张表连起来,查出每个学生的姓名和成绩。 - 把一个新同学插入到学生表中。
- 把某个学生的班级信息更新为新值。
- 删除某条成绩记录。
这9个练习做完,你对SQL基本操作就已经有了完整的体感。接下来再深入学习子查询、CASE WHEN条件判断、窗口函数这些进阶内容就会顺手很多。
我个人在实际带人过程中的体会是:SQL入门阶段,最容易出问题的不是语法记不住,而是“逻辑还没想清楚就急着写代码”。写SQL之前,先用一两句话把需求说清楚:我要从哪些表取什么数据,过滤条件是什么,是否需要分组,排序规则是什么。说清楚了,写出来的SQL基本不会有大问题。
最后再分享一个小技巧:遇到SQL报了看不懂的错,先把错误信息整个复制到搜索框里。SQL的报错信息通常都很直白,比如“near 'xxx': syntax error”,几乎直接告诉你是哪一段语法出了问题。自己先试着读一遍错误提示,再去搜索引擎查,比直接问别人更能加深印象。哪怕英文不好,看多了也就习惯了。
SQL这条路很长,但起点不高。把查询和增删改这四类操作练到条件反射的程度,再往后面的性能优化、数据建模去走,你会发现之前所有的基础工作都不会白费。希望这篇实战笔记能帮你走好第一步。