先看两句话,写法几乎一样,结果天差地别:
-- 写法 A:函数包住「字段」
select * from msg where date(sendtime) = '2017-11-19';
-- 写法 B:函数不碰「字段」
select * from msg where sendtime >= '2017-11-19' and sendtime < '2017-11-20';
语义完全一样,都是「查 2017-11-19 这天的留言。
但性能可能差 1000 倍。 因为:
- 写法 A:
date(sendtime)对每一行 都要算一次函数,索引彻底失效 → 全表扫描 - 写法 B:
sendtime是「裸字段」,可以直接在索引上做范围查找 → 扫几行就完事
这就是本篇要反复强调的一条铁律:
函数写在「字段」身上,索引就死了;函数写在「常量」身上,索引还活着。
记住这句话,你已经比 90% 的人更懂 SQL 了。下面我们按课件顺序,把四类函数一个一个过。
1. 全景 + 实验环境
MySQL 内置函数大致分四类:
| 类别 | 代表函数 | 典型用途 |
|---|---|---|
| 日期函数 | current_date、now、date_add、datediff |
时间过滤、时间计算 |
| 字符串函数 | concat、length、substring、replace |
拼接、截取、统计 |
| 数学函数 | abs、ceiling、floor、format、rand |
取整、格式化、随机 |
| 其他函数 | user、md5、database、ifnull |
权限排查、摘要、兜底 |
实验环境(照着课件建好,跟着敲一遍):
create database if not exists func_demo default character set utf8mb4;
use func_demo;
-- 案例一:生日表
create table tmp (
id int primary key auto_increment,
birthday date
);
-- 案例二:留言表
create table msg (
id int primary key auto_increment,
content varchar(30) not null,
sendtime datetime
);
-- 后面字符串函数要用到(用一个模拟 EMP 的表)
create table emp (
empno int primary key,
ename varchar(20),
job varchar(20),
sal decimal(7,2),
deptno int
);
insert into emp values
(7369, 'SMITH', 'CLERK', 800.00, 20),
(7499, 'ALLEN', 'SALESMAN', 1600.00, 30),
(7521, 'WARD', 'SALESMAN', 1250.00, 30),
(7566, 'JONES', 'MANAGER', 2975.00, 20),
(7654, 'MARTIN', 'SALESMAN', 1250.00, 30),
(7698, 'BLAKE', 'MANAGER', 2850.00, 30),
(7782, 'CLARK', 'MANAGER', 2450.00, 10),
(7788, 'SCOTT', 'ANALYST', 3000.00, 20),
(7839, 'KING', 'PRESIDENT',5000.00, 10),
(7844, 'TURNER', 'SALESMAN', 1500.00, 30),
(7876, 'ADAMS', 'CLERK', 1100.00, 20),
(7900, 'JAMES', 'CLERK', 950.00, 30),
(7902, 'FORD', 'ANALYST', 3000.00, 20),
(7934, 'MILLER', 'CLERK', 1300.00, 10);
-- 成绩表(字符串拼接案例要用)
create table student (
id int primary key auto_increment,
name varchar(20) not null,
chinese float default 0,
math float default 0,
english float default 0
);
insert into student (name, chinese, math, english) values
('唐三藏', 67, 98, 56), ('孙悟空', 87, 78, 77), ('猪悟能', 88, 98, 90);
🔗 前后照应 :上一篇我们建的
exam_result表就在这里演化成了student。同一个套路:float default 0------ 保证表达式参与计算时不会被 NULL 污染。
2. 日期函数
2.1 取当前时间的三兄弟
select current_date();
+----------------+
| current_date() |
+----------------+
| 2017-11-19 |
+----------------+
select current_time();
+----------------+
| current_time() |
+----------------+
| 13:51:21 |
+----------------+
select current_timestamp();
+---------------------+
| current_timestamp() |
+---------------------+
| 2017-11-19 13:51:48 |
+---------------------+
一张表把「三兄弟」和它们的别名全记下来:
| 你要什么 | 标准写法 | 等价别名 | 返回类型 | 示例 |
|---|---|---|---|---|
| 只要日期 | current_date() |
curdate() |
DATE |
2017-11-19 |
| 只要时间 | current_time() |
curtime() |
TIME |
13:51:21 |
| 日期 + 时间 | current_timestamp() |
now() / localtime() |
DATETIME |
2017-11-19 13:51:48 |
⚠️ 括号可以省略 :select current_date;、select now; 也能跑(因为这些是「无参函数」)。但建议永远写括号------不写括号容易和「列名」混淆,可读性差。
补全:now() 和 sysdate() 的区别(面试常考)
select now(), sysdate(), sleep(2), now(), sysdate();
| 函数 | 返回的是什么时刻 | 一条 SQL 里多次调用 |
|---|---|---|
now() |
整条语句开始执行的时刻 | 每次都一样(语句内恒定) |
sysdate() |
函数被调用那一瞬间的时刻 | 可能不一样(会随时间变化) |
为什么重要? 在一条耗时较长的 SQL 里(比如大批量 insert ... select),now() 会给所有行同一个时间戳(这通常正是你要的 :一批数据同一个创建时间);sysdate() 会给出各不相同的、飘忽的时间。
工程建议:99% 的场景用 now()。 它可预测、可复现,主从复制也安全。
💡 顺带一个冷知识:
now()的返回值受会话时区影响。
set time_zone = '+00:00'; select now(); -- 会变成 UTC 时间这就是为什么**「服务器时区没统一」会导致数据看起来差了 8 小时**------不是数据错了,是你看到的「now()」变了。
2.2 日期加减:date_add / date_sub
-- 在日期的基础上加日期
select date_add('2017-10-28', interval 10 day);
+-----------------------------------------+
| date_add('2017-10-28', interval 10 day) |
+-----------------------------------------+
| 2017-11-07 |
+-----------------------------------------+
-- 在日期的基础上减去时间
select date_sub('2017-10-1', interval 2 day);
+---------------------------------------+
| date_sub('2017-10-1', interval 2 day) |
+---------------------------------------+
| 2017-09-29 |
+---------------------------------------+
语法骨架:
date_add(日期, interval 数量 单位)
date_sub(日期, interval 数量 单位)
⚠️ 三个必须牢记的语法细节:
① interval 是关键字,不能写成 INTERVAL(...) 函数形式
-- ❌ 错误
select date_add('2017-10-28', interval(10) day);
-- ✅ 正确
select date_add('2017-10-28', interval 10 day);
② 单位(unit)不带引号,且是「单数」
interval 1 day -- ✅
interval 1 days -- ⚠️ 能跑(MySQL 容错),但不规范
interval 1 'day' -- ❌ 报错
③ 支持的所有单位(背下来,用的时候不用查文档)
| 类别 | 单位 |
|---|---|
| 时间单位 | microsecond、second、minute、hour、day、week、month、quarter、year |
| 复合单位 | day_hour、day_minute、day_second、hour_minute、hour_second、minute_second、year_month |
-- 复合单位要写成 'D H:M:S' 这种字符串格式
select date_add('2017-11-19 10:00:00', interval '1 02:30:00' day_second);
-- 2017-11-20 12:30:00
补全:date_sub 其实可以直接用 date_add 加负数实现
select date_add('2017-10-1', interval -2 day); -- 2017-09-29,和 date_sub 一样
date_add 和 date_sub 是同一套逻辑的两个方向。 记住 date_add 一个就够了。
补全:真实业务里的三大高频用法
-- ① 「最近 7 天」
select * from orders where create_time >= date_sub(now(), interval 7 day);
-- ② 「下个月的今天」(注意月末的坑)
select date_add('2017-01-31', interval 1 month); -- 2017-02-28(自动修正到月末)
-- ③ 「本月的第一天」
select date_sub(current_date(), interval day(current_date()) - 1 day);
-- 或者更简单:
select date_format(current_date(), '%Y-%m-01');
注意第 ② 条的「月末溢出」行为:
| 原日期 | +1 month | 结果 |
|---|---|---|
2017-01-31 |
2017-02-28 ✅(不是 3-3!) |
|
2017-03-31 |
2017-04-30 ✅ |
|
2017-01-30 |
2017-02-28 ✅ |
MySQL 会「夹」到目标月的最后一天,不会溢出到下个月。这个设计很贴心,但你要知道它存在(某些数据库会报错或溢出)。
2.3 日期差:datediff
select datediff('2017-10-10', '2016-9-1');
+------------------------------------+
| datediff('2017-10-10', '2016-9-1') |
+------------------------------------+
| 404 |
+------------------------------------+
datediff(日期1, 日期2) = 日期1 − 日期2,单位是「天」。
三个关键细节:
① 参数顺序:是「前者减后者」
select datediff('2017-10-10', '2016-9-1'); -- 404
select datediff('2016-9-1', '2017-10-10'); -- -404
很多人写反了,结果拿到负数还以为出错。
② 只算「日期」部分,忽略时分秒
select datediff('2017-10-10 23:59:59', '2017-10-09 00:00:01');
-- 1(只算日期差,不管时间)
③ 它是「天数差」,不是「经过多少小时」
如果两个时间只差 23 小时但跨了日期 ,datediff 会返回 1:
select datediff('2017-10-10 00:00:01', '2017-10-09 01:00:00');
-- 1(其实只过了 23 小时零 1 秒)
补全:要精确到小时/分钟/秒,用 timestampdiff
-- timestampdiff(单位, 开始, 结束) → 结束 - 开始
select timestampdiff(second, '2017-10-09 01:00:00', '2017-10-10 00:00:01'); -- 82801
select timestampdiff(hour, '2017-10-09 01:00:00', '2017-10-10 00:00:01'); -- 23
select timestampdiff(minute, '2017-10-09 01:00:00', '2017-10-10 00:00:01'); -- 1380
datediff vs timestampdiff 对比:
| 对比 | datediff(d1, d2) |
timestampdiff(unit, d1, d2) |
|---|---|---|
| 单位 | 只能天 | 秒/分/时/天/周/月/年... |
| 精度 | 只看日期 | 精确到时间 |
| 负数 | 支持 | 支持 |
| 小数 | 不支持 | 不支持(会截断取整) |
| 常用 | 「相差几天」 | 「相差多少小时」 |
⚠️
timestampdiff的取整是「截断」而不是「四舍五入」:
select timestampdiff(hour, '2017-10-09 00:00:00', '2017-10-09 01:59:59'); -- 1(不是 2)差 1 小时 59 分 59 秒,返回 1。这对「剩余时间」类计算是安全的(向下取整),但对「计费」类计算要注意。
2.4 案例一:生日表
-- 创建一张表,记录生日
create table tmp (
id int primary key auto_increment,
birthday date
);
-- 添加当前日期
insert into tmp (birthday) values (current_date());
select * from tmp;
+----+------------+
| id | birthday |
+----+------------+
| 1 | 2017-11-19 |
+----+------------+
一条最简单的 insert,但有三个知识点:
① birthday 是 date 类型,只存日期
insert into tmp (birthday) values (now()); -- 也能插,但只保留日期部分
insert into tmp (birthday) values ('2017-11-19'); -- 字符串会隐式转成 date
⚠️ 用
now()插date列时,时间部分会被静默截掉 。严格模式下不会报错,但你可能以为存进去的是完整时间------所以列类型和值类型要匹配 ,date列就传日期,datetime列再传完整时间。
② 补全:date 列也能写默认值(MySQL 8.0.13+)
-- 8.0.13 之前:不支持表达式默认值
-- 8.0.13 之后:
alter table tmp modify birthday date default (current_date);
insert into tmp () values (); -- birthday 自动填今天
🔗 前后照应 :这就是上一篇(表的约束)讲 DEFAULT 时提到的「8.0.13 起支持默认值表达式」。当时只是提了一句,这里你看到了它在日期场景下的实战价值:在建表层面就把「默认今天」这件事固定下来,比每次 insert 都记得写要可靠得多。
③ 「算年龄」怎么写?
-- 用 timestampdiff 算周岁
select id, birthday, timestampdiff(year, birthday, current_date()) as age from tmp;
-- 但要注意:timestampdiff(year, ...) 是「按整年截断」的近似
-- 严格算周岁(生日还没到就减 1):
select id, birthday,
timestampdiff(year, birthday, current_date()) -
(date_format(current_date(), '%m%d') < date_format(birthday, '%m%d')) as real_age
from tmp;
这就是「函数组合求解真实业务问题」的典型方式------单个函数都很简单,组合起来能解决很复杂的问题。
2.5 案例二:留言表(本节最有价值)
create table msg (
id int primary key auto_increment,
content varchar(30) not null,
sendtime datetime
);
insert into msg (content, sendtime) values ('hello1', now());
insert into msg (content, sendtime) values ('hello2', now());
select * from msg;
+----+---------+---------------------+
| id | content | sendtime |
+----+---------+---------------------+
| 1 | hello1 | 2017-11-19 14:12:20 |
| 2 | hello2 | 2017-11-19 14:13:21 |
+----+---------+---------------------+
2.5.1 需求一:只显示日期,不显示时间
select content, date(sendtime) from msg;
+---------+----------------+
| content | date(sendtime) |
+---------+----------------+
| hello1 | 2017-11-19 |
| hello2 | 2017-11-19 |
+---------+----------------+
date(expr) 提取日期部分。 同类还有:
| 函数 | 提取什么 | 示例 |
|---|---|---|
date('2017-11-19 14:12:20') |
日期 | 2017-11-19 |
time('2017-11-19 14:12:20') |
时间 | 14:12:20 |
year(...) |
年 | 2017 |
month(...) |
月 | 11 |
day(...) |
日 | 19 |
hour(...) |
时 | 14 |
minute(...) |
分 | 12 |
second(...) |
秒 | 20 |
这个用法在 SELECT 列表里完全没问题 (不影响索引,见第 6 节)。但放到 WHERE 里就出大事了------这正是 2.8 节要讲的。
2.5.2 需求二:查询在 2 分钟内发布的帖子
课件的写法,还有一张绝妙的示意图:
select * from msg where date_add(sendtime, interval 2 minute) > now();
------------------------------|-----------|-------------|------------------
初始时间 now() 初始时间+2min
这张图我要放大讲,因为它体现了一个非常好的思维模型:
「2 分钟内发布」=「发布时间 + 2 分钟 还在未来」
用时间轴画出来:
过去 ←───────────────────────────────────────────→ 现在(now())
│
sendtime sendtime+2min
│ │
├────────── 2 分钟 ──────────┤
│ │
┌───────┴────────────────────────────┴──────┐
│ 如果 sendtime+2min > now() │
│ 说明这条消息发布还不到 2 分钟 │
└───────────────────────────────────────────┘
这个「平移时间轴」的思路,比「算差值」更优雅,因为:
- 算差值需要「对每行做减法和比较」
- 平移时间轴只需要「做一次加法,然后比较大小」
但工程上,这个写法有一个致命问题:函数包住了字段 sendtime,索引会失效。
2.5.3 三种写法,性能差几个数量级
❌ 写法一(课件版,语义正确但不推荐上生产)
select * from msg where date_add(sendtime, interval 2 minute) > now();
问题 :date_add(sendtime, ...) 让 sendtime 参与运算,索引无法使用 。虽然 MySQL 优化器在部分情况下能推导出等价的 sendtime > now() - interval 2 minute,但不要赌------它不一定做这个推导。
✅ 写法二(推荐:把函数挪到常量身上)
select * from msg where sendtime > now() - interval 2 minute;
语义完全一样 (sendtime > now() - 2min ⇔ sendtime + 2min > now()),但 sendtime 现在裸奔在比较符左边,可以直接走索引。
❌ 写法三(最慢,绝对不要写)
select * from msg where timestampdiff(second, sendtime, now()) < 120;
问题双重叠加 :① 函数包住字段;② timestampdiff 还要对每行做跨类型计算。全表扫描 + 逐行函数调用,是最慢的一种。
三种写法的性能对照(假设 sendtime 上有索引、表有 100 万行):
| 写法 | 是否走索引 | 扫描行数 | 相对耗时 |
|---|---|---|---|
date_add(sendtime, 2min) > now() |
❌ 大概率不保证 | 100 万 | 🐢🐢🐢 |
sendtime > now() - interval 2 minute |
✅ 走范围扫描 | 几行 | 🚀 |
timestampdiff(second, sendtime, now()) < 120 |
❌ 一定不走 | 100 万 | 🐢🐢🐢🐢 |
一句话总结这一节:
把函数从「字段」身上,挪到「常量」身上。 让字段单独站在比较符的一侧,索引就活了。
2.6 补全:date / datetime / timestamp 到底怎么选
这是课件没讲、但建表时天天要面对的选择题。
| 类型 | 存储 | 范围 | 是否随时区变化 | 占用 |
|---|---|---|---|---|
date |
只有日期 | 1000-01-01 ~ 9999-12-31 |
否 | 3 字节 |
time |
只有时间 | -838:59:59 ~ 838:59:59 |
否 | 3 字节 |
datetime |
日期 + 时间 | 1000-01-01 00:00:00 ~ 9999-12-31 23:59:59 |
否(原样存原样取) | 8 字节(5.6.4 前) |
timestamp |
日期 + 时间 | 1970-01-01 00:00:01 UTC ~ 2038-01-19 03:14:07 UTC |
是(按会话时区自动转换) | 4 字节 |
year |
只有年 | 1901 ~ 2155 |
否 | 1 字节 |
这是全篇最容易踩的「看不见的坑」,我分三点讲清:
① timestamp 有「2038 问题」
timestamp 用的是 32 位有符号整数存 Unix 时间戳,最大值是 2038-01-19 03:14:07。超过这个时间点会溢出。
create table t (ts timestamp);
insert into t values ('2040-01-01 00:00:00');
-- ERROR 1292 (22007): Incorrect datetime value
这不是危言耸听 :2025 年了,还有 13 年。很多老系统的「会员到期时间」字段就是
timestamp,如果你的业务有「20 年有效期」,现在就会炸。工程建议:需要存「未来很久之后」的时间,一律用
datetime。
② datetime 不随时区变化,timestamp 会
set time_zone = '+08:00';
insert into t1 (dt) values ('2017-11-19 10:00:00'); -- datetime 列
insert into t2 (ts) values ('2017-11-19 10:00:00'); -- timestamp 列
set time_zone = '+00:00';
select dt from t1; -- 2017-11-19 10:00:00 (没变)
select ts from t2; -- 2017-11-19 02:00:00 (变了!减了 8 小时)
timestamp 存的是 UTC,取的时候按当前会话时区转换 ;datetime 存什么就是什么(相当于「本地时间快照」)。
③ 那到底怎么选?我给的工程结论:
| 场景 | 推荐 | 理由 |
|---|---|---|
| 创建时间 / 更新时间 | datetime |
不随时区漂移,可读性好,没有 2038 限制 |
| 需要跨时区展示的绝对时刻 | timestamp 或 datetime(存 UTC) |
但更推荐统一存 UTC 的 datetime |
| 生日 / 节假日 | date |
只需要日期 |
| 只想存「几点几分」 | time |
比如营业时间 |
| 高并发、表极宽、要省空间 | timestamp |
4 字节 vs 8 字节,且会自动带时区语义 |
⚠️ 行业最佳实践(大厂主流):
全库统一用
datetime,所有时间一律存 UTC,展示层再按用户时区转。理由:① 避开 2038;② 避开「服务器时区配置不一致」的经典事故;③ 迁移到任何数据库都不踩坑。
-- 建表时的标准三字段(阿里规约也是这个思路)
create table orders (
id bigint unsigned primary key auto_increment,
create_time datetime not null default current_timestamp comment '创建时间(UTC)',
update_time datetime not null default current_timestamp on update current_timestamp comment '更新时间(UTC)'
);
🔗 前后照应 :看到
default current_timestamp on update current_timestamp了吗?这是上一篇(约束)讲的 DEFAULT + ON UPDATE ,也是本篇日期函数和约束的完美结合------用「默认值」把「自动记时间」这件事固化在表结构里,应用层永远不用操心。
2.7 补全:date_format / str_to_date / timestampdiff 等必备函数
课件只讲了 6 个日期函数,但实际工作里至少还要会这 5 个。
2.7.1 date_format:日期 → 字符串
select date_format(now(), '%Y-%m-%d %H:%i:%s');
-- 2017-11-19 14:12:20
select date_format(now(), '%Y年%m月%d日');
-- 2017年11月19日
select date_format(now(), '%W'); -- Sunday(星期全名)
select date_format(now(), '%Y-%m'); -- 2017-11(做「按月统计」的 key!)
最常用的占位符(务必背下来):
| 占位符 | 含义 | 示例 |
|---|---|---|
%Y |
四位年 | 2017 |
%y |
两位年 | 17 |
%m |
两位月 | 11 |
%c |
月(无前导 0) | 11 |
%d |
两位日 | 19 |
%e |
日(无前导 0) | 19 |
%H |
24 小时制时 | 14 |
%h |
12 小时制时 | 02 |
%i |
分 (注意不是 %M!) |
12 |
%s |
秒 | 20 |
%W |
星期全名 | Sunday |
%w |
星期数字(0=周日) | 0 |
%j |
一年中的第几天 | 323 |
⚠️ 最容易错的一个 :分钟是
%i,不是%M!%M是「月份的全名(英文)」,%m才是「两位月份数字」。 写成'%Y-%m-%d %H:%M:%S',结果会得到2017-11-19 14:November:20------ 你能想象这种诡异输出吗?面试里专门有人挖这个坑。
业务高频用法:
-- 按月统计(报表必备)
select date_format(create_time, '%Y-%m') as ym, count(*)
from orders group by ym;
-- 按小时统计(监控必备)
select date_format(create_time, '%Y-%m-%d %H') as ymdh, count(*)
from logs group by ymdh;
2.7.2 str_to_date:字符串 → 日期
select str_to_date('2017-11-19', '%Y-%m-%d');
-- 2017-11-19(DATE 类型)
select str_to_date('2017年11月19日 14:12', '%Y年%m月%d日 %H:%i');
-- 2017-11-19 14:12:00
用途 :从外部导入的数据(Excel、CSV)日期格式五花八门,用 str_to_date 统一成 MySQL 的标准格式存进去。
date_format 和 str_to_date 是一对反函数:
date_format
日期类型 ─────────────→ 字符串
←─────────────
str_to_date
2.7.3 timestampdiff:细粒度时间差
见 2.3 节,这里补一个「计算耗时」的实战:
-- 算每个订单的处理耗时(秒)
select order_id,
timestampdiff(second, create_time, finish_time) as cost_seconds
from orders;
2.7.4 last_day / dayofweek / weekday / dayofyear
select last_day('2017-11-19'); -- 2017-11-30(本月最后一天,做「月末结算」必用)
select dayofweek('2017-11-19'); -- 1(1=周日, 2=周一, ..., 7=周六)
select weekday('2017-11-19'); -- 6(0=周一, ..., 6=周日)
select dayofyear('2017-11-19'); -- 323(今年的第几天)
⚠️ dayofweek 和 weekday 的编号规则完全不同,别混:
| 函数 | 周日 | 周一 | 周六 |
|---|---|---|---|
dayofweek() |
1 | 2 | 7 |
weekday() |
7 | 0 | 6 |
算「本周一」的标准写法:
select date_sub(current_date(), interval weekday(current_date()) day);
2.7.5 unix_timestamp / from_unixtime:时间戳互转
select unix_timestamp(now()); -- 1511071948
select from_unixtime(1511071948); -- 2017-11-19 14:12:28
select from_unixtime(1511071948, '%Y-%m-%d'); -- 2017-11-19
用途:和 Java/Go/Python 后端交互时,前端常传时间戳;和 Redis、日志系统交互时也常用。
🔗 前后照应 :课件开头有一句「获得时间戳」,其实就是
current_timestamp()(注意:它返回的是 DATETIME,不是 Unix 时间戳! 这是教材里一个容易误导的表述)。 真正的时间戳(秒数)要用unix_timestamp()。
2.8 ⚠️ 日期函数最大的坑:where date(col) = ?
这是本篇的核心警告,请务必看懂。
❌ 写法 A(99% 的人第一反应都会这么写)
select * from msg where date(sendtime) = '2017-11-19';
看起来很自然:「我只关心日期,所以对字段取个日期再比。」
但它的执行过程是这样的:
对于表的每一行:
① 取 sendtime
② 调用 date() 算出日期
③ 和 '2017-11-19' 比较
④ 符合就留下
索引(B+ 树)是按 sendtime 的原始值排序的,不是按 date(sendtime) 排序的。 所以 MySQL 拿到 date(sendtime) 这个表达式,根本没法在树上定位,只能全表扫。
✅ 写法 B(改成范围查询)
select * from msg
where sendtime >= '2017-11-19 00:00:00'
and sendtime < '2017-11-20 00:00:00';
为什么这样对? 因为条件变成了「sendtime 在某个区间内」,而 sendtime 在索引里是有序的------可以直接二分定位起点,扫到终点就停。
⚠️ 注意是 < 第二天 而不是 <= 当天 23:59:59 ,因为 datetime 的精度可能到微秒:
-- ❌ 会漏掉 23:59:59.500 这样的数据
where sendtime <= '2017-11-19 23:59:59'
-- ✅ 正确
where sendtime < '2017-11-20 00:00:00'
这是「左闭右开」原则在日期范围上的应用,请刻进肌肉记忆。
补全:如果你确实需要「按日期查」且不想改 SQL------用生成列 + 索引
MySQL 5.7+ 支持「生成列(Generated Column)」,可以给 date(sendtime) 建一个真实存在的列(虚拟列也能加索引):
alter table msg
add column send_date date as (date(sendtime)) virtual,
add index idx_send_date (send_date);
-- 现在这个查询走索引了
select * from msg where send_date = '2017-11-19';
这是「既要可读性、又要性能」的最佳解法,也是 MySQL 8.0 时代推荐的做法。
3. 字符串函数
3.1 charset:看字符集
select charset(ename) from emp;
+----------------+
| charset(ename) |
+----------------+
| utf8mb4 |
| utf8mb4 |
| ... |
+----------------+
charset() 返回字符串的字符集。 平时用得少,主要在两种场景:
- 排查乱码:「为什么我这个中文存进去变成问号了?」------先看字符集对不对
- 确认迁移影响:「这张表是 utf8 还是 utf8mb4?」
补全:配套的两个函数
select collation('abc'); -- utf8mb4_general_ci(排序规则)
select charset('中文'); -- utf8mb4
💡
utf8和utf8mb4的区别(重要)
名称 最大字节 能存 emoji 吗 utf8/utf8mb33 字节 ❌ 不能 utf8mb44 字节 ✅ 能 MySQL 里的
utf8是「残缺的 UTF-8」 (只有 3 字节),存 emoji、部分生僻字会报错或截断。 建库建表一律用utf8mb4------这已经是行业铁律,别再写utf8了。
3.2 concat:拼接(以及 NULL 的传染性)
select concat(name, '的语文是', chinese, '分,数学是', math, '分') as '分数' from student;
+------------------------------------------+
| 分数 |
+------------------------------------------+
| 唐三藏的语文是67分,数学是98分 |
| 孙悟空的语文是87分,数学是78分 |
| 猪悟能的语文是88分,数学是98分 |
+------------------------------------------+
这是 SELECT 里最常用的字符串函数,没有之一。 做「报表拼接」「展示字段组合」全靠它。
⚠️ 但这里有个致命的 NULL 陷阱:
select concat('张三', null, '的成绩');
-- NULL !!!
concat 只要有一个参数是 NULL,整个结果就是 NULL。
🔗 前后照应 :这正是我们前两篇反复讲的「NULL 会污染整个表达式」,第三个战场:
- 第 05 篇:
1 + null = null- 第 06 篇:
chinese + math + english里有一列 NULL 就整个变 NULL- 第 07 篇:
concat里有一个 NULL 就整个变 NULL
那怎么办?两个方案:
-- 方案一:用 ifnull 兜底(推荐)
select concat(ifnull(name, ''), '的语文是', ifnull(chinese, 0), '分') from student;
-- 方案二:用 concat_ws(它专门处理 NULL)
select concat_ws('-', name, chinese) from student;
-- concat_ws 会跳过 NULL,但分隔符不能是 NULL
concat vs concat_ws 对比:
| 函数 | 语法 | 遇 NULL 时 |
|---|---|---|
concat(a, b, c) |
直接拼接 | 整个结果变 NULL |
concat_ws(sep, a, b, c) |
用 sep 连接 |
自动跳过 NULL |
select concat('a', null, 'c'); -- NULL
select concat_ws('-', 'a', null, 'c'); -- a-c(跳过了 null)
工程建议 :拼接可能为 NULL 的字段时,优先用 concat_ws,或用 ifnull 兜底。 否则你的报表会突然出现一片空白。
3.3 length vs char_length:字节 vs 字符
select length(name), name from student;
+-------------+-----------+
| length(name) | name |
+-------------+-----------+
| 9 | 唐三藏 |
| 9 | 孙悟空 |
| 9 | 猪悟能 |
+-------------+-----------+
为什么「唐三藏」是 3 个字,length 却返回 9?
因为课件这段注释说得非常准确:
注意:
length函数返回字符串长度,以字节为单位。如果是多字节字符则计算多个字节数;如果是单字节字符则算作一个字节。比如:字母、数字算作一个字节,中文表示多个字节数(与字符集编码有关)。
在 utf8mb4 下:
| 字符 | 字节数 |
|---|---|
英文字母 a |
1 |
数字 1 |
1 |
| 中文「唐」 | 3 |
| emoji 😀 | 4 |
所以 length('唐三藏') = 3 × 3 = 9。
但如果你要的是「几个字」,得用 char_length:
select length(name), char_length(name), name from student;
+-------------+------------------+-----------+
| length(name) | char_length(name) | name |
+-------------+------------------+-----------+
| 9 | 3 | 唐三藏 |
| 9 | 3 | 孙悟空 |
| 9 | 3 | 猪悟能 |
+-------------+------------------+-----------+
对比表(这张要背下来):
| 函数 | 单位 | '唐三藏' |
'abc' |
'😀'(utf8mb4) |
|---|---|---|---|---|
length() |
字节 | 9 | 3 | 4 |
char_length() / character_length() |
字符 | 3 | 3 | 1 |
什么时候用哪个?
| 场景 | 用哪个 | 原因 |
|---|---|---|
| 校验「昵称不超过 20 个字」 | char_length |
用户感知的是「字符数」 |
| 校验「字段不超 64 字节」 | length |
存储/协议限制都是按字节 |
| 统计文本占用的磁盘空间 | length |
存储是字节 |
| 统计「有几个逗号」(本篇 OJ) | length |
单字节字符时等长 |
⚠️
varchar(20)里的 20 是「字符数」还是「字节数」?是字符数! (MySQL 5.0.3 起)
varchar(20)在utf8mb4下最多占20 × 4 = 80字节。🔗 前后照应 :我们第 05 篇专门对比过「
int(10)的 10 是显示宽度(没意义)vsvarchar(20)的 20 是真实长度上限」。这里再补一刀:varchar(20)的 20 是字符数,不是字节数。
3.4 replace:替换
-- 将 EMP 表中所有名字中有 S 的替换成 '上海'
select replace(ename, 'S', '上海'), ename from emp;
+---------------------------+-------+
| replace(ename, 'S', '上海') | ename |
+---------------------------+-------+
| 上海MITH | SMITH |
| ALLEN | ALLEN |
| ... | ... |
| 上海COTT | SCOTT |
+---------------------------+-------+
replace(str, from_str, to_str):把 str 里所有的 from_str 换成 to_str。
三个关键细节:
① 它是「全部替换」,不是「只换第一个」
select replace('a-b-c-d', '-', '_'); -- a_b_c_d(全换了)
② 它是「字面子串替换」,不是正则
select replace('abc123', '1', 'X'); -- abcX23
select replace('abc123', '12', 'YY'); -- abcYY3
③ 大小写敏感性取决于排序规则(collation)
-- utf8mb4_general_ci(默认,大小写不敏感)
select replace('SMITH', 's', 'X'); -- XMITH(小写 s 也能匹配大写 S!)
⚠️ 这是一个非常容易踩的坑 :在 _ci 结尾的 collation 下,replace 会忽略大小写。要精确替换,得用二进制比较:
-- 只替换大写 S
select replace(binary 'SMITH', 's', 'X'); -- SMITH(没变)
select replace('SMITH' collate utf8mb4_bin, 's', 'X'); -- SMITH(没变)
💡 这个坑和上一篇(唯一键)讲过的「
utf8mb4_general_ci下Tom@qq.com和tom@qq.com会冲突」是同一个根源 ------_ci= case insensitive。同一个知识,两处踩坑。
补全:replace 的第四个参数?没有!
有些人以为 replace 能指定「替换第几个」,它不能 。要按位置替换,必须用 substring 拼:
-- 把第 2 个字符替换成 X
select concat(substring('abcde', 1, 1), 'X', substring('abcde', 3));
-- aXcde
3.5 substring:截取
-- 截取 EMP 表中 ename 字段的第二个到第三个字符
select substring(ename, 2, 2), ename from emp;
+--------------------+-------+
| substring(ename,2,2) | ename |
+--------------------+-------+
| MI | SMITH |
| LL | ALLEN |
| AR | WARD |
| ON | JONES |
+--------------------+-------+
substring(str, pos, len):从第 pos 个字符开始,取 len 个字符。
⚠️ 最重要的一个细节:pos 从 1 开始,不是从 0!
select substring('abcdef', 1, 3); -- abc(从第 1 个开始)
select substring('abcdef', 0, 3); -- 空字符串!(从 0 开始是无效的)
select substring('abcdef', 2, 2); -- bc
这一点和编程语言完全相反 (Java 的
substring(0,3)、JS 的slice(0,3)都是从 0 开始)。 写 SQL 时切换不过来,是新手最常见的 bug 之一。
三种参数形式:
| 形式 | 含义 | 示例 |
|---|---|---|
substring(str, pos) |
从 pos 截到末尾 |
substring('abcdef', 3) → cdef |
substring(str, pos, len) |
从 pos 截 len 个 |
substring('abcdef', 3, 2) → cd |
substring(str, -pos) |
从右往左数 | substring('abcdef', -3) → def |
负数 pos 从末尾往前数,这个技巧非常实用:
-- 取文件扩展名(最后一个点之后)
select substring('report.pdf', -3); -- pdf
-- 但更严谨的是用 substring_index
select substring_index('report.pdf', '.', -1); -- pdf
别名:substr() 和 mid() 都是 substring() 的同义词。 用哪个都行,substring 最标准。
补全:left / right 更简单的截取
select left('abcdef', 3); -- abc(取左边 3 个)
select right('abcdef', 3); -- def(取右边 3 个)
substring(str, 1, n) 就是 left(str, n),后者可读性更好。
3.6 lcase / ucase:大小写
-- 以首字母小写的方式显示所有员工的姓名
select concat(lcase(substring(ename, 1, 1)), substring(ename, 2)) from emp;
+-----------------------------------------------------------+
| concat(lcase(substring(ename,1,1)), substring(ename,2)) |
+-----------------------------------------------------------+
| sMITH |
| aLLEN |
| wARD |
| jONES |
+-----------------------------------------------------------+
这个案例是本节最精彩的一段,因为它把「三个函数组合」讲透了:
ename = 'SMITH'
↓ ① substring(ename, 1, 1) → 'S'(取首字母)
↓ ② lcase('S') → 's'(转小写)
↓ ③ substring(ename, 2) → 'MITH'(取第二个到最后)
↓ ④ concat('s', 'MITH') → 'sMITH'(拼起来)
函数列表:
| 函数 | 作用 | 别名 |
|---|---|---|
lcase(str) |
转小写 | lower(str) |
ucase(str) |
转大写 | upper(str) |
lcase / ucase 是 Oracle 风格,lower / upper 是标准 SQL。 用哪个都行,团队内统一即可。
补全:upper / lower 也受 collation 影响?
不完全是。upper/lower 的行为取决于字符集是否支持大小写映射------对中文、数字、符号,它们什么都不做(原样返回)。
select lower('ABC123中文'); -- abc123中文
3.7 补全:另外 10 个必须会的字符串函数
课件只讲了 6 个,实际工作至少还要会这些。
| 函数 | 作用 | 示例 |
|---|---|---|
trim(str) |
去首尾空格 | trim(' a ') → 'a' |
ltrim / rtrim |
只去左 / 只去右 | ltrim(' a') → 'a' |
left / right |
取左/右 n 个字符 | left('abcdef', 2) → 'ab' |
instr(str, sub) |
子串第一次出现的位置(1 起) | instr('hello', 'l') → 3 |
locate(sub, str) |
同上(参数顺序相反) | locate('l', 'hello') → 3 |
lpad / rpad |
左/右补足到指定长度 | lpad('5', 3, '0') → '005' |
reverse(str) |
翻转 | reverse('abc') → 'cba' |
repeat(str, n) |
重复 n 次 | repeat('ab', 3) → 'ababab' |
space(n) |
n 个空格 | space(3) → ' ' |
substring_index |
按分隔符取第 n 段 | substring_index('a,b,c', ',', 2) → 'a,b' |
regexp_like / regexp_replace |
正则(8.0+) | regexp_like('abc', '^a') → 1 |
其中 substring_index 是「解析逗号分隔字符串」的神器,务必掌握:
select substring_index('10,A,B', ',', 1); -- 10 (前 1 段)
select substring_index('10,A,B', ',', 2); -- 10,A (前 2 段)
select substring_index('10,A,B', ',', -1); -- B (最后 1 段)
select substring_index('10,A,B', ',', -2); -- A,B (后 2 段)
实战用法:
-- ① 取邮箱的域名部分
select substring_index(email, '@', -1) from users;
-- ② 取 URL 的最后一段
select substring_index('/api/v1/users/123', '/', -1); -- 123
lpad 也有个实战场景 :给编号补零(正好替代第 05 篇讲的被弃用的 zerofill!)
🔗 前后照应 :第 05 篇我们讲过,
zerofill在 MySQL 8.0 被弃用,官方建议用lpad()替代。这里你就看到具体写法了:
select lpad(a, 5, '0') from t; -- 1 → 00001同一个知识点,从「为什么要弃用」到「弃用后怎么写」,两篇博客把它闭环了。
3.8 性能:字符串函数同样会让索引失效
规则和第 2 节一模一样:函数包住字段 = 索引失效。
-- ❌ 索引失效(对字段做了函数运算)
select * from users where left(phone, 3) = '138';
select * from users where substr(name, 1, 1) = '张';
select * from users where upper(name) = 'TOM';
-- ✅ 走索引
select * from users where phone like '138%'; -- 前缀匹配,可用索引
select * from users where name like '张%';
like '138%' 为什么能用索引,而 left(phone,3)='138' 不能?
因为 like '前缀%' 在语义上等价于范围查询 :phone >= '138' and phone < '139',而范围查询可以直接在 B+ 树上定位。
这就是「能用 LIKE 前缀匹配,就别用函数截取」的原因。
⚠️ 但注意有个例外 :upper/lower 在大小写不敏感 的 collation 下其实是冗余的,因为 where name = 'tom' 本身就不区分大小写。这又是一个 _ci 的坑。
4. 数学函数
4.1 abs / ceiling / floor / format / rand
select abs(-100.2);
+-------------+
| abs(-100.2) |
+-------------+
| 100.2 |
+-------------+
select ceiling(23.04);
+---------------+
| ceiling(23.04) |
+---------------+
| 24 |
+---------------+
select floor(23.7);
+------------+
| floor(23.7) |
+------------+
| 23 |
+------------+
select format(12.3456, 2);
+--------------------+
| format(12.3456, 2) |
+--------------------+
| 12.35 |
+--------------------+
select rand();
+---------------------+
| rand() |
+---------------------+
| 0.19086026123860724 |
+---------------------+
五个函数,一句话各自记住:
| 函数 | 作用 | 示例 |
|---|---|---|
abs(x) |
绝对值 | abs(-100.2) → 100.2 |
ceiling(x) / ceil(x) |
向上取整 | ceiling(23.04) → 24 |
floor(x) |
向下取整 | floor(23.7) → 23 |
format(x, d) |
保留 d 位小数 + 千分位 ,返回字符串 |
format(12.3456, 2) → '12.35' |
rand() |
返回 [0, 1) 的随机数 |
0.1908602... |
⚠️ ceiling 和 floor 的名字要分清:
- ceiling = 天花板 → 往上取(往大了去)
- floor = 地板 → 往下取(往小了去)
记不住的记忆法:站在房间里,天花板在你头上(大),地板在你脚下(小)。
4.2 面试题:round / format / truncate 的区别
这三个都是「处理小数」,但行为完全不同,是经典面试题:
select round(12.3456, 2); -- 12.35 (返回数值,四舍五入)
select format(12.3456, 2); -- '12.35' (返回字符串,四舍五入 + 千分位)
select truncate(12.3499, 2); -- 12.34 (返回数值,直接截断,不四舍五入!)
| 对比 | round(x, d) |
format(x, d) |
truncate(x, d) |
|---|---|---|---|
| 返回类型 | 数值 | 字符串 | 数值 |
| 舍入方式 | 四舍五入 | 四舍五入 | 直接截断 |
| 千分位 | ❌ | ✅ | ❌ |
12.3456, 2 |
12.35 |
'12.35' |
12.34 |
1234567.891, 2 |
1234567.89 |
'1,234,567.89' |
1234567.89 |
⚠️ format 返回字符串,这是个隐藏陷阱:
-- format 返回的是字符串,参与运算会被隐式转换
select format(12.3456, 2) + 1;
-- 13.35(MySQL 把 '12.35' 转成 12.35 再算,但精度可能出问题)
-- ⚠️ 更坑的是这个:
select format(1234.5678, 2) + 1;
-- 千分位也带进去了!'1,234.57' + 1 会怎样?
结论:
format只用于「展示」,绝不用于「计算」。 要做金额计算,用decimal类型 +round,或者干脆在应用层格式化。
补全:format(x, 0) 的小坑
select format(12.3456, 0); -- '12'(没有小数点)
select round(12.3456, 0); -- 12
看起来一样,但 format 返回 '12'(字符串),round 返回 12(数值)。
4.3 负数取整的坑
这是符号处理的经典陷阱,很多人没意识到:
select ceiling(-1.5); -- -1 (往上取,-1 > -1.5)
select floor(-1.5); -- -2 (往下取,-2 < -1.5)
select ceiling(-0.5); -- 0
select floor(-0.5); -- -1
⚠️ 注意:ceiling(-1.5) = -1,绝对值反而变小了!
很多人以为「向上取整」= 「数值变大」,其实准确的说法是「向正无穷方向取整」:
数轴方向: -2 -1.5 -1 0 1 1.5 2
↓ ↑
floor(-1.5) ceiling(-1.5)
= -2 = -1
这在实际业务里的影响:
-- 分页计算:「总共 25 条,每页 10 条,需要几页?」
select ceiling(25 / 10); -- 3 ✅
select ceiling(0 / 10); -- 0 ⚠️(0 条数据也需要「0 页」,看起来对)
select ceiling(-1 / 10); -- 0 ⚠️(如果有负数,就出错了)
所以分页计算一定要保证被除数非负,否则结果会很奇怪。
补全:round 对负数的四舍五入
select round(-1.5); -- -2(远离零方向舍入)
select round(-1.4); -- -1
select round(1.5); -- 2
select round(2.5); -- 3
MySQL 的 round 对 .5 是**「远离零舍入」**(round half away from zero),不是银行家舍入法。
⚠️ 但如果你处理的是浮点数 (
float/double),round的行为可能因为二进制表示误差而不符合直觉。金额一律用decimal。
4.4 rand() 的三种用法与一种灾难
用法一:取随机数
select rand(); -- 0.19086026123860724
select rand() * 100; -- 19.086026123860724
用法二:固定种子(可复现)
select rand(1); -- 每次执行都是同一个值!
select rand(1), rand(1), rand(2);
给 rand 传参数就是「设种子」 ,同一个种子每次生成相同的序列。这在测试、复现 bug、生成固定的假数据时非常有用。
用法三:随机取整
-- 取 [0, 99] 的随机整数
select floor(rand() * 100);
-- 取 [1, 100] 的随机整数
select floor(rand() * 100) + 1;
❌ 灾难:order by rand()
🔗 前后照应:上一篇(CRUD)3.5.7 我们专门讲过这个坑,这里从「函数视角」再说一遍:
select * from goods order by rand() limit 10;为什么慢? 因为
rand()要为全表每一行 生成一个随机数,然后整体排序(Using temporary; Using filesort),最后取前 10 个。一张 100 万行的表,这个查询可能要跑好几秒,而且完全无法用索引。
正确做法见上一篇 3.5.7 节(游标分页 + 随机 offset、或预生成随机列)。
5. 其它函数
5.1 user() vs current_user()
select user();
+----------------+
| user() |
+----------------+
| root@localhost |
+----------------+
看起来平平无奇,但这里藏着一个面试级别的细节:
select user(), current_user();
| 函数 | 返回什么 |
|---|---|
user() |
你连接时「声明」的用户和主机 |
current_user() |
MySQL 实际认证通过的用户 |
为什么会有区别? 因为 MySQL 有「匿名用户」「权限重定向」这类机制。举个经典例子:
-- 你连接时写的是 mysql -u root -p
select user(); -- root@localhost
select current_user(); -- 可能是 'root'@'localhost',也可能是匿名用户 ''
排查权限问题时,这两个都要看。
-- 生产环境排查「我的 SQL 为什么没权限」
select user(), current_user(), database();
-- 然后对照 mysql.user 表看这个账号有哪些权限
补全:session_user() 和 system_user()
-
session_user()等于user() -
system_user()返回 MySQL 服务进程的操作系统账户 + 认证方式(8.0+)select system_user(); -- root@localhost on using Socket
5.2 md5():摘要不是加密
select md5('admin');
+----------------------------------+
| md5('admin') |
+----------------------------------+
| 21232f297a57a5a743894a0e4a801fc3 |
+----------------------------------+
md5() 把任意长度的字符串「摘要」成固定的 32 位十六进制字符串。
⚠️ 但请务必记住一句话:
MD5 是「摘要(Hash)」,不是「加密(Encryption)」。 摘要是不可逆的,而且------MD5 已经被攻破了。
为什么不能用 MD5 存密码?三个原因:
① 「彩虹表」可以反查
select md5('admin'); -- 21232f297a57a5a743894a0e4a801fc3
这个哈希值在网上一搜就能查到原文是 admin。因为相同的输入永远产生相同的输出,攻击者可以预先算好海量「常用密码 → 哈希」的对照表(彩虹表),直接反查。
② MD5 碰撞已被实证
2004 年王小云团队就证明了 MD5 的碰撞攻击。「两个不同的文件可以有相同的 MD5」------这在文件校验场景是致命的。
③ 计算太快,暴力破解成本极低
现代 GPU 每秒能算几十亿次 MD5。8 位密码的穷举现在是「分钟级」的事。
✅ 那密码到底该怎么存?
| 方案 | 安全性 | 说明 |
|---|---|---|
| 明文 | ☠️ 绝对不行 | 库一泄全玩完 |
md5(密码) |
❌ 不行 | 彩虹表 + 碰撞 |
md5(md5(密码)) |
❌ 不行 | 只是「多一层」,彩虹表照样能打 |
md5(密码 + 固定盐) |
⚠️ 勉强 | 防住彩虹表,但防不住针对性穷举 |
bcrypt / scrypt / argon2 |
✅ 正确做法 | 自带随机盐 + 可调计算成本 |
正确姿势(应用层实现,不在 SQL 里):
// Java 示例(BCrypt)
String hash = BCrypt.hashpw(rawPassword, BCrypt.gensalt(12)); // 存这个 hash
boolean ok = BCrypt.checkpw(inputPassword, hash); // 校验
// Node.js(argon2)
const hash = await argon2.hash(password);
const ok = await argon2.verify(hash, input);
为什么这些算法更好?
- 自带随机盐:同一个密码每次哈希结果都不同,彩虹表彻底失效
- 可调成本(cost):可以故意让计算变慢(比如 100ms/次),攻击者穷举成本指数级上升
- 专为密码设计:内存硬(memory-hard),抵抗 GPU/ASIC 加速
一句话结论:数据库里只存「密码哈希」(bcrypt/argon2 生成),不存 md5,更不存明文。
md5()的正确用途是「文件/内容校验」(比如比对两份数据是否一致),不是密码。🔗 前后照应 :第 05 篇我们讲「约束是最后一道防线」------密码安全也是同一个思维:不指望某一道防线完美,而是让攻击者的代价高到不划算。
5.3 password():这个函数已经不存在了
select password('root');
+-------------------------------------------+
| password('root') |
+-------------------------------------------+
| *81F5E21E35407D884A6CD4A731AEBFB6AF209E1B |
+-------------------------------------------+
⚠️ 重大版本提醒:这个函数在 MySQL 8.0 里已经被彻底移除了。
| 版本 | password() 状态 |
|---|---|
| MySQL 5.6 及以前 | ✅ 可用(返回 * + 40 位 SHA1 大写十六进制) |
| MySQL 5.7.6 起 | ⚠️ deprecated(弃用,会警告) |
| MySQL 8.0 起 | ❌ 已移除,调用报错 |
-- MySQL 8.0 下执行
select password('root');
-- ERROR 1305 (42000): FUNCTION func_demo.password does not exist
这说明课件是基于 5.6/5.7 写的。 如果你在 8.0 上照着敲,一定会报错。
那 8.0 里「给用户设密码」怎么写?
-- 创建用户并设密码(8.0 推荐:caching_sha2_password)
create user 'test'@'localhost' identified by 'Test@123';
-- 修改密码
alter user 'test'@'localhost' identified by 'NewPwd@456';
-- 5.7 的老写法(已不推荐)
-- set password for 'test'@'localhost' = password('Test@123');
8.0 默认的认证插件是 caching_sha2_password (比 5.7 的 mysql_native_password 更安全),但这也导致了一个著名的兼容性问题:老版本的客户端驱动连不上 8.0 (报 Authentication plugin 'caching_sha2_password' cannot be loaded)。
解决办法(仅仅为了兼容旧驱动):
alter user 'test'@'localhost' identified with mysql_native_password by 'Test@123';
-- 注意:8.0.34+ 起 mysql_native_password 也被标记为 deprecated
5.4 database() / version() / connection_id()
select database();
+------------+
| database() |
+------------+
| func_demo |
+------------+
database() 返回当前正在使用的数据库(没选数据库时返回 NULL)。
别小看它,排查事故时非常有用:
-- 事故现场:连错了库,把测试数据插进了生产库
select database(), user(), @@hostname, connection_id();
补全:几个实用的「诊断函数」
| 函数 | 返回 | 用途 |
|---|---|---|
database() |
当前库名 | 确认没连错库 |
version() |
MySQL 版本 | 确认环境(5.7 还是 8.0) |
connection_id() |
当前连接 ID | 排查「是谁在锁表」 |
last_insert_id() |
上一次自增 id | 🔗 第 05 篇讲自增时用过 |
row_count() |
上一次 DML 影响行数 | 🔗 第 06 篇讲 ON DUPLICATE KEY 时用过 |
@@sql_mode |
当前 SQL 模式 | 排查严格模式 |
@@time_zone |
当前时区 | 排查时间差 8 小时 |
sleep(n) |
睡眠 n 秒 | 测试慢查询、复现锁等待 |
sleep() 的经典用法------复现并发问题:
-- 会话 A:模拟慢事务
start transaction;
update account set balance = balance - 100 where id = 1;
select sleep(10); -- 卡住 10 秒,不提交
commit;
-- 会话 B:这时来查同一行,就会「锁等待超时」
select * from account where id = 1;
-- ERROR 1205 (HY000): Lock wait timeout exceeded
这是排查死锁/锁等待的标准手法。
5.5 ifnull / coalesce / nullif 三兄弟
select ifnull('abc', '123');
+----------------------+
| ifnull('abc', '123') |
+----------------------+
| abc |
+----------------------+
select ifnull(null, '123');
+---------------------+
| ifnull(null, '123') |
+---------------------+
| 123 |
+---------------------+
ifnull(v1, v2):v1 为 NULL 就返回 v2,否则返回 v1。
这是本篇最实用的函数 ,因为它是「NULL 兜底」的标准工具。
三个兄弟对比:
| 函数 | 语义 | 示例 |
|---|---|---|
ifnull(a, b) |
a 为 NULL 返回 b,否则返回 a |
ifnull(null, 0) → 0 |
coalesce(a, b, c, ...) |
返回第一个非 NULL 的值 | coalesce(null, null, 3) → 3 |
nullif(a, b) |
a = b 时返回 NULL,否则返回 a |
nullif(0, 0) → NULL |
coalesce 是 ifnull 的加强版 (支持多参数),标准 SQL 写法,跨数据库通用:
-- ifnull 只能兜两层
select ifnull(a, 'default');
-- coalesce 可以兜多层:先看 a,再看 b,再看 c,最后兜底
select coalesce(a, b, c, 'default');
nullif 的实战场景:防止「除零错误」
-- 计算「及格率」:如果总人数是 0,除法会得到 NULL(不是报错,但结果不好看)
select sum(case when score >= 60 then 1 else 0 end) / count(*) as pass_rate from exam;
-- count(*) 不可能为 0,但如果是「某科目平均分」,分子可能为 NULL
select sum(score) / nullif(count(*), 0) from exam;
-- 当 count(*)=0 时,nullif 返回 NULL,整个除法变成 NULL(而不是报错/异常值)
🔗 前后照应 :第 06 篇我们讲过 ------
SUM()/AVG()在没有匹配行时返回NULL,而COUNT()返回0。当时我给了个兜底写法ifnull(avg(score), 0)。这里补上更通用的coalesce版本:
select coalesce(avg(score), 0) from exam where 1 = 0;同一个知识点,从「为什么会是 NULL」到「怎么优雅地兜底」,两篇博客正好接上。
ifnull 在这个场景下最重要:字符串拼接
-- ❌ 有一个 NULL 就全变 NULL
select concat(name, '的电话是', phone) from users;
-- ✅ 兜底
select concat(name, '的电话是', ifnull(phone, '未填写')) from users;
6. 全篇最重要:函数与索引的战争
好了,四类函数都讲完了。现在我要把全篇的「主线」收拢起来。
这条主线就是:函数会让索引失效。
6.1 索引失效的三大凶手
经过 06、07 两篇的积累,你已经见过全部三个「凶手」了:
| # | 凶手 | 反例 | 正解 |
|---|---|---|---|
| ① | 函数包住字段 | where date(create_time) = '2017-11-19' |
where create_time >= '2017-11-19' and create_time < '2017-11-20' |
| ② | 前导通配符 | where name like '%张%' |
where name like '张%' 或改用全文索引/ES |
| ③ | 隐式类型转换 | where varchar_col = 123 |
where varchar_col = '123' |
🔗 前后照应:
- 凶手 ① 是本篇 的主角(
date()、substring()、upper()...)- 凶手 ② 在第 06 篇 3.4.4 节讲
like时讲过- 凶手 ③ 在第 06 篇 4.6 节讲
UPDATE时讲过三篇博客,凑齐了索引失效的完整版图。 这就是我为什么建议你按顺序读------知识是长在一棵树上的,不是散落在地上的碎片。
6.2 为什么函数会让索引失效?(原理)
B+ 树索引的本质是**「把字段的值排好序,做成一棵可以二分查找的树」**。
索引里存的是: create_time 的原始值,按大小排好序
│
┌───────────┼───────────┐
2017-11-18 2017-11-19 2017-11-20
当你说 where date(create_time) = '2017-11-19' 时,MySQL 需要的是「date(create_time) 排好序的树」------但索引里没有这棵树 ,只有「create_time 的树」。
所以它没法二分,只能:
扫第 1 行 → 算 date() → 比一下
扫第 2 行 → 算 date() → 比一下
...
扫第 100 万行 → 算 date() → 比一下
全表扫描 + 100 万次函数调用。
而 where create_time >= A and create_time < B,MySQL 可以直接在树上二分定位到 A,然后顺着叶子节点往后扫到 B ------ 扫几十行就停。
6.3 「让字段裸奔」原则
我把整篇的工程结论浓缩成一句话:
把函数写在「常量」身上,让「字段」单独站在比较符的一侧。
| ❌ 函数包字段 | ✅ 字段裸奔 |
|---|---|
where date(t) = '2017-11-19' |
where t >= '2017-11-19' and t < '2017-11-20' |
where year(t) = 2017 |
where t >= '2017-01-01' and t < '2018-01-01' |
where date_add(sendtime, interval 2 minute) > now() |
where sendtime > now() - interval 2 minute |
where left(phone, 3) = '138' |
where phone like '138%' |
where upper(name) = 'TOM' |
where name = 'tom'(_ci 下自动忽略大小写) |
where substr(code, 1, 2) = 'AB' |
where code like 'AB%' |
这张表贴在你工位旁边,能省下无数次「为什么这个查询这么慢」的加班。
6.4 但也要知道:函数不是「绝对不能用在 WHERE 里」
别矫枉过正。 有三种情况,函数在 WHERE 里是没问题的:
① 函数作用在「常量」上
-- ✅ 函数只算一次,和行数无关,索引照样能用
where create_time > date_sub(now(), interval 7 day);
② 表很小(几千行以内)
全表扫和索引查的差别可以忽略。不要为了「理论上更快」把 SQL 写得不可读。
③ 你确实需要函数的结果,且有函数索引
-- MySQL 8.0.13+ 支持函数索引(Functional Index)
alter table msg add index idx_date ((date(sendtime)));
-- 现在这个也能走索引了
select * from msg where date(sendtime) = '2017-11-19';
④ 生成列 + 索引(见 2.8 节)
alter table msg add column send_date date as (date(sendtime)) virtual,
add index idx_send_date (send_date);
⑤ 用小表驱动查询,让函数只在少量行上执行
-- 先筛出时间范围(走索引),再在外层做字符串处理
select * from (
select * from orders where create_time >= '2017-11-19' and create_time < '2017-11-20'
) t where left(remark, 3) = 'VIP';
⑥ 用 EXPLAIN 确认,而不是凭感觉
explain select * from msg where date(sendtime) = '2017-11-19';
-- 看 type 列:ALL = 全表扫描(坏);range/ref = 走索引(好)
-- 看 rows 列:估算扫描行数
-- 看 key 列:实际用了哪个索引(NULL = 没用索引)
这是本篇最后也最重要的一条建议:不要背「XX 会索引失效」,要会用
EXPLAIN验证。 MySQL 的优化器很聪明,有时候你以为失效了它反而用上了(比如索引条件下推 ICP、覆盖索引)。一切以EXPLAIN为准。
7. 实战 OJ
7.1 牛客:查找字符串 '10,A,B' 中逗号 ',' 出现的次数 cnt
题目 :查找字符串 '10,A,B' 中逗号 ',' 出现的次数 cnt。
这是本篇唯一的 OJ 题,但它考的是「用函数解决问题的思维」。
解法一(标准解法,首选):总长度 − 去掉逗号后的长度
select length('10,A,B') - length(replace('10,A,B', ',', '')) as cnt;
+-----+
| cnt |
+-----+
| 2 |
+-----+
思路拆解:
原串: '10,A,B' length = 6
替换后: '10AB' length = 4 (逗号被删掉了)
差 : 6 - 4 = 2 (正好是逗号的个数)
为什么这个思路成立? 因为 replace 把每一个逗号都「删掉」了,每删一个,长度就少 1(前提是逗号是单字节字符)。
⚠️ 关键前提:必须是「单字节字符」
-- 如果统计的是「中文逗号」,也一样(中文逗号 , 是 3 字节)
select length('10,A,B') - length(replace('10,A,B', ',', '')); -- 6(3 字节 × 2 ≠ 2)
所以更严谨的写法是用 char_length(按字符数算):
select char_length('10,A,B') - char_length(replace('10,A,B', ',', '')) as cnt;
-- 2 ✅(不管逗号是几个字节都成立)
🔗 前后照应 :这就是 3.3 节那张「
lengthvschar_length」表格的实战价值。课堂上例子恰好是单字节逗号,两个函数结果一样;但换成中文标点就翻车。
解法二:用 substring_index 反推
-- 思路:能用 ',' 切出 n+1 段,说明有 n 个逗号
select substring_index('10,A,B', ',', -1); -- B
-- 但 substring_index 不直接返回段数,得配合循环(SQL 不擅长)
这个思路在纯 SQL 里不好实现(需要循环),所以本题标准答案就是解法一。
解法三:如果数据在表里(真实场景)
-- 假设 t 表有一列 s,要统计每行里逗号的个数
select id, s,
char_length(s) - char_length(replace(s, ',', '')) as comma_cnt
from t;
这个「差值法」是一个通用技巧,能解决一大类「统计某字符出现次数」的问题:
| 需求 | SQL |
|---|---|
统计 s 里 x 的个数 |
char_length(s) - char_length(replace(s, x, '')) |
| 统计单词个数(空格分隔) | char_length(s) - char_length(replace(s, ' ', '')) + 1 |
| 判断是否包含某字符 | instr(s, x) > 0 或 s like '%x%' |
8. 内置函数速查大表
这一张表覆盖了本篇 + 常用扩展,建议收藏。
8.1 日期时间函数
| 函数 | 作用 | 示例 → 结果 |
|---|---|---|
current_date() / curdate() |
今天 | → 2017-11-19 |
current_time() / curtime() |
此刻时间 | → 13:51:21 |
current_timestamp() / now() |
此刻日期时间 | → 2017-11-19 13:51:48 |
sysdate() |
函数执行瞬间 | 语句内会变化 |
date_add(d, interval n unit) |
加 | +10 day → 2017-11-07 |
date_sub(d, interval n unit) |
减 | -2 day → 2017-09-29 |
datediff(d1, d2) |
相差天数 | → 404 |
timestampdiff(unit, d1, d2) |
相差(细粒度) | hour,... → 23 |
date(x) |
取日期部分 | → 2017-11-19 |
time(x) |
取时间部分 | → 14:12:20 |
year/month/day/hour/minute/second(x) |
取各分量 | year() → 2017 |
date_format(d, fmt) |
日期 → 字符串 | '%Y-%m-%d' |
str_to_date(s, fmt) |
字符串 → 日期 | '%Y-%m-%d' |
last_day(d) |
本月最后一天 | → 2017-11-30 |
dayofweek(d) |
周几(1=周日) | → 1 |
weekday(d) |
周几(0=周一) | → 6 |
dayofyear(d) |
一年第几天 | → 323 |
unix_timestamp() |
→ Unix 秒 | → 1511071948 |
from_unixtime(n) |
Unix 秒 → 时间 | → 2017-11-19 14:12:28 |
8.2 字符串函数
| 函数 | 作用 | 示例 → 结果 |
|---|---|---|
charset(s) |
字符集 | → utf8mb4 |
collation(s) |
排序规则 | → utf8mb4_general_ci |
concat(a, b, ...) |
拼接(遇 NULL 全 NULL) | → 唐三藏的语文是67分 |
concat_ws(sep, a, b, ...) |
带分隔符拼接(跳过 NULL) | → a-b-c |
length(s) |
字节数 | '唐三藏' → 9 |
char_length(s) |
字符数 | '唐三藏' → 3 |
replace(s, from, to) |
全部替换 | 'S'→'上海' → 上海MITH |
substring(s, pos[, len]) |
截取(pos 从 1 起) | (ename,2,2) → MI |
left(s, n) / right(s, n) |
左/右取 n 个 | → ab |
lcase / lower |
小写 | 'S' → s |
ucase / upper |
大写 | 's' → S |
trim / ltrim / rtrim |
去空格 | → a |
instr(s, sub) |
子串位置 | 'hello','l' → 3 |
locate(sub, s) |
同上(参数反) | → 3 |
lpad(s, n, pad) / rpad |
左右补足 | lpad('5',3,'0') → 005 |
reverse(s) |
翻转 | 'abc' → cba |
repeat(s, n) |
重复 | 'ab',3 → ababab |
substring_index(s, sep, n) |
按分隔符取第 n 段 | 'a,b,c',',',-1 → c |
regexp_like(s, pat) |
正则匹配(8.0+) | → 1 |
8.3 数学函数
| 函数 | 作用 | 示例 → 结果 |
|---|---|---|
abs(x) |
绝对值 | -100.2 → 100.2 |
ceiling(x) / ceil |
向上取整 | 23.04 → 24 |
floor(x) |
向下取整 | 23.7 → 23 |
round(x, d) |
四舍五入(数值) | 12.3456,2 → 12.35 |
format(x, d) |
四舍五入 + 千分位(字符串) | 12.3456,2 → '12.35' |
truncate(x, d) |
截断(不四舍五入) | 12.3499,2 → 12.34 |
rand([seed]) |
随机数 [0,1) |
→ 0.19086... |
mod(a, b) |
取余 | 10,3 → 1 |
pow(a, b) |
乘方 | → 8 |
sqrt(x) |
开方 | 16 → 4 |
8.4 其他函数
| 函数 | 作用 | 示例 → 结果 |
|---|---|---|
user() |
连接时声明的用户 | → root@localhost |
current_user() |
实际认证用户 | → root@localhost |
database() |
当前库 | → func_demo |
version() |
版本 | → 8.0.36 |
connection_id() |
当前连接 ID | → 8 |
md5(s) |
MD5 摘要(32 位) | 'admin' → 21232f29... |
ifnull(a, b) |
a 为 NULL 返回 b | null,'123' → 123 |
coalesce(a, b, c...) |
第一个非 NULL | null,null,3 → 3 |
nullif(a, b) |
a=b 返回 NULL | 0,0 → NULL |
last_insert_id() |
上次自增 id | 🔗 第 05 篇 |
row_count() |
上次影响行数 | 🔗 第 06 篇 |
sleep(n) |
睡 n 秒(测试用) | --- |
⚠️ 已移除/弃用的函数,不要再用:
函数 状态 password()8.0 已移除 md5()存密码❌ 不安全 zerofill(属性)8.0.17 起弃用,用 lpad()替代(🔗 第 05 篇)utf8字符集建议改 utf8mb4
9. 常见坑与报错速查
| 现象 / 报错 | 原因 | 解决 |
|---|---|---|
ERROR 1305: FUNCTION xxx does not exist |
调用了不存在的函数(8.0 里 password() 已移除) |
改用 8.0 的写法 |
ERROR 1582: Incorrect parameter count in call to native function |
函数参数个数不对 | 查文档核对参数 |
concat 结果整列变成 NULL |
某个参数是 NULL | 用 ifnull 兜底或改 concat_ws |
length 返回的数字比预期大 |
它是字节数,中文占 3 字节 | 改用 char_length |
date_format 的分钟显示成英文月份 |
用了 %M 而不是 %i |
分钟是 %i |
substring 少取了一个字符 |
pos 从 1 开始,不是 0 |
改下标 |
datediff 算出负数 |
参数顺序反了 | datediff(大, 小) |
| 日期查询很慢(全表扫描) | where date(col) = ? 函数包住字段 |
改范围查询 >= and < |
| 日期范围少了最后一天的数据 | 用了 <= '23:59:59' |
改 < 次日 00:00:00 |
timestamp 插入 2040 年报错 |
2038 上限 | 改用 datetime |
| 时间差了 8 小时 | timestamp 列 + 会话时区不一致 |
统一存 UTC,或改 datetime |
format() 参与运算结果怪 |
format 返回字符串且带千分位 |
只用于展示,计算用 round |
ceiling(-1.5) 得到 -1 以为是 bug |
向上取整是「向正无穷」 | 理解数轴方向 |
order by rand() 巨慢 |
全表逐行算随机数 + 排序 | 换随机策略(🔗 第 06 篇) |
select password('root') 在 8.0 报错 |
函数已移除 | 用 alter user ... identified by |
10. 面试高频 10 问
Q1:length() 和 char_length() 的区别? length() 返字节数 (中文 3 字节、emoji 4 字节),char_length() 返字符数。校验「昵称几个字」用后者。
Q2:now() 和 sysdate() 的区别? now() 返回语句开始执行 的时刻,语句内恒定;sysdate() 返回函数被调用瞬间的时刻,语句内可能变化。
Q3:datediff 和 timestampdiff 的区别? datediff 只能算「相差几天」,只看日期部分;timestampdiff(unit, a, b) 可以算秒/分/时/天...的差值,精确到时间。
Q4:为什么不建议用 where date(create_time) = '2017-11-19'? 因为函数包住字段,B+ 树索引按原始值排序,无法用表达式定位 → 全表扫描。应改为 create_time >= '2017-11-19' and create_time < '2017-11-20'(左闭右开)。
Q5:为什么日期范围要用 < 次日 而不是 <= 当天 23:59:59? 因为 datetime 精度可达微秒,<= 23:59:59 会漏掉 23:59:59.500 这类数据。
Q6:datetime 和 timestamp 的区别?怎么选? datetime 8 字节、范围到 9999 年、不随时区变化 ;timestamp 4 字节、范围到 2038 年 、随时区转换 。工程建议:统一用 datetime 存 UTC,避开 2038 和时区坑。
Q7:round、format、truncate 的区别? round 四舍五入返回数值;format 四舍五入 + 千分位,返回字符串 ;truncate 直接截断不四舍五入。
Q8:concat 遇到 NULL 会怎样?怎么办? 整个结果变 NULL。用 ifnull(字段, '') 兜底,或改用 concat_ws(它会跳过 NULL)。
Q9:MD5 存密码安全吗? 不安全。MD5 已被攻破(彩虹表可反查、碰撞已被实证),正确做法是 bcrypt / scrypt / argon2(自带随机盐 + 可调成本)。
Q10:user() 和 current_user() 的区别? user() 是连接时声明 的用户@主机;current_user() 是实际认证通过 的账号。排查权限问题要看 current_user()。
11. 总结:一张图 + 一句口诀
┌─────────────────────────┐
│ MySQL 内置函数 │
└─────────────────────────┘
│
┌──────────────┬──────────────┼──────────────┬──────────────┐
│ │ │ │ │
【日期】 【字符串】 【数学】 【其他】 【索引影响】
│ │ │ │ │
now() concat abs() user() ⚠️ 函数包住
curdate() length ceiling() md5() 字段 =
date_add char_length floor() database() 🔴 索引失效
date_sub replace round() ifnull()
datediff substring format() coalesce()
timestampdiff lcase/ucase truncate() nullif()
date_format lpad rand() last_day()
str_to_date substring_index
│ │ │ │
└──────────────┴──────────────┴──────────────┘
│
┌─────────┴─────────┐
│ ✅ 正确姿势 │
│ 让「字段」裸奔 │
│ 让「函数」算常量 │
└───────────────────┘
一句口诀:
「日期算区间、字符串看字节、数学防截断、函数别碰字段。」
展开:
| 字 | 含义 |
|---|---|
| 日期算区间 | 日期过滤用 >= ... and < ... 范围查询,绝不用 date(col) = ? |
| 字符串看字节 | length 是字节,char_length 是字符,拼 NULL 全变 NULL |
| 数学防截断 | round 四舍五入、format 返字符串、truncate 直接砍 |
| 函数别碰字段 | 把函数挪到常量身上,字段单独站在比较符一侧 |
最后,回顾一下这三篇博客串起的主线:
| 篇 | 主题 | 核心问题 | 一句话 |
|---|---|---|---|
| 第 05 篇 | 表的约束 | 数据能不能进来 | 约束是门禁,脏数据进不来 |
| 第 06 篇 | 增删改查 | 数据怎么进出 | 执行顺序决定一切 |
| 第 07 篇 | 内置函数 | 数据怎么加工 | 函数别碰字段,索引就活了 |
三篇合起来,是你"敢让 SQL 上生产"的完整底座。
而它们背后其实只有三条共同的底层规律:
- NULL 会传染 ------
1 + null、concat(null)、NOT IN (null)、count(col),到处都一样。 - 索引怕表达式 ------函数包字段、前导
%、隐式转换,三个凶手。 - 顺序决定语义 ------
WHERE早于GROUP BY早于SELECT早于ORDER BY;函数在字段上早于函数在常量上。
把这三条吃透,你会发现 MySQL 绝大多数"玄学问题",其实都是这三条规律的某种变形。
附:一键可执行完整脚本
-- ============================================================
-- MySQL 内置函数 · 全功能演示脚本
-- 环境:MySQL 5.7 / 8.0 均可(标注 8.0 的除外)
-- ============================================================
drop database if exists func_demo;
create database func_demo default character set utf8mb4;
use func_demo;
-- ---------- 实验表 ----------
create table tmp (
id int primary key auto_increment,
birthday date
);
create table msg (
id int primary key auto_increment,
content varchar(30) not null,
sendtime datetime
);
create table emp (
empno int primary key,
ename varchar(20),
job varchar(20),
sal decimal(7,2),
deptno int
);
insert into emp values
(7369,'SMITH','CLERK',800.00,20),(7499,'ALLEN','SALESMAN',1600.00,30),
(7521,'WARD','SALESMAN',1250.00,30),(7566,'JONES','MANAGER',2975.00,20),
(7654,'MARTIN','SALESMAN',1250.00,30),(7698,'BLAKE','MANAGER',2850.00,30),
(7782,'CLARK','MANAGER',2450.00,10),(7788,'SCOTT','ANALYST',3000.00,20),
(7839,'KING','PRESIDENT',5000.00,10),(7844,'TURNER','SALESMAN',1500.00,30),
(7876,'ADAMS','CLERK',1100.00,20),(7900,'JAMES','CLERK',950.00,30),
(7902,'FORD','ANALYST',3000.00,20),(7934,'MILLER','CLERK',1300.00,10);
create table student (
id int primary key auto_increment,
name varchar(20) not null,
chinese float default 0,
math float default 0,
english float default 0
);
insert into student (name, chinese, math, english) values
('唐三藏',67,98,56),('孙悟空',87,78,77),('猪悟能',88,98,90);
-- ---------- 2. 日期函数 ----------
select current_date(), current_time(), current_timestamp();
select curdate(), curtime(), now();
select date_add('2017-10-28', interval 10 day); -- 2017-11-07
select date_sub('2017-10-1', interval 2 day); -- 2017-09-29
select date_add('2017-10-1', interval -2 day); -- 2017-09-29(等价)
select datediff('2017-10-10', '2016-9-1'); -- 404
select timestampdiff(hour, '2017-10-09 01:00:00', '2017-10-10 00:00:01'); -- 23
-- 案例一:生日表
insert into tmp (birthday) values (current_date());
select * from tmp;
select timestampdiff(year, birthday, current_date()) as age from tmp;
-- 案例二:留言表
insert into msg (content, sendtime) values ('hello1', now());
insert into msg (content, sendtime) values ('hello2', now());
select content, date(sendtime) from msg;
-- 「2 分钟内」三种写法(推荐第二种)
select * from msg where date_add(sendtime, interval 2 minute) > now();
select * from msg where sendtime > now() - interval 2 minute;
select * from msg where timestampdiff(second, sendtime, now()) < 120;
-- ⚠️ 索引失效 vs 走索引
-- select * from msg where date(sendtime) = '2017-11-19'; -- ❌ 全表扫
-- select * from msg where sendtime >= '2017-11-19' and sendtime < '2017-11-20'; -- ✅
-- explain select * from msg where date(sendtime) = '2017-11-19'; -- 看 type=ALL
-- 生成列 + 索引的解法(5.7+)
alter table msg add column send_date date as (date(sendtime)) virtual,
add index idx_send_date (send_date);
-- 8.0.13+ 函数索引
-- alter table msg add index idx_date ((date(sendtime)));
-- 8.0.13+ 日期列默认值
-- alter table tmp modify birthday date default (current_date);
-- ---------- 3. 字符串函数 ----------
select charset('abc'), collation('abc');
select concat(name, '的语文是', chinese, '分,数学是', math, '分') as '分数' from student;
select length(name), char_length(name), name from student;
select replace(ename, 'S', '上海'), ename from emp;
select substring(ename, 2, 2), ename from emp;
select concat(lcase(substring(ename, 1, 1)), substring(ename, 2)) from emp;
-- 补全常用函数
select trim(' a '), left('abcdef',3), right('abcdef',3);
select instr('hello','l'), locate('l','hello'), reverse('abc'), repeat('ab',3);
select lpad('5',3,'0'), rpad('5',3,'0');
select substring_index('10,A,B', ',', 1), substring_index('10,A,B', ',', -1);
select substring_index('a@b.com', '@', -1);
-- concat 的 NULL 陷阱
select concat('a', null, 'c'); -- NULL
select concat_ws('-', 'a', null, 'c'); -- a-c
-- ---------- 4. 数学函数 ----------
select abs(-100.2), ceiling(23.04), floor(23.7);
select format(12.3456, 2), round(12.3456, 2), truncate(12.3499, 2);
select rand(), rand(1), floor(rand() * 100) + 1;
select ceiling(-1.5), floor(-1.5);
select format(1234567.891, 2); -- '1,234,567.89'
-- ---------- 5. 其它函数 ----------
select user(), current_user(), database(), version(), connection_id();
select md5('admin'); -- 21232f297a57a5a743894a0e4a801fc3
select ifnull('abc', '123'); -- abc
select ifnull(null, '123'); -- 123
select coalesce(null, null, 3); -- 3
select nullif(0, 0); -- NULL
select concat(name, '的电话是', ifnull(null, '未填写')) from student limit 1;
-- last_insert_id / row_count(呼应第 05、06 篇)
create table t_auto (id int primary key auto_increment, v varchar(10));
insert into t_auto (v) values ('a'),('b'),('c');
select last_insert_id(); -- 1(批量取第一个)
select row_count(); -- 3
-- ---------- 7. 实战 OJ ----------
-- 统计 '10,A,B' 中逗号的个数
select length('10,A,B') - length(replace('10,A,B', ',', '')) as cnt; -- 2
select char_length('10,A,B') - char_length(replace('10,A,B', ',', '')) as cnt; -- 2
-- ---------- 8. 补充:时间戳互转 ----------
select unix_timestamp(now());
select from_unixtime(1511071948);
select from_unixtime(1511071948, '%Y-%m-%d %H:%i:%s');
如果这篇博客帮你理清了某个点,欢迎点赞收藏;有任何疑问,评论区见,我会一条条回。