☰
MySQL中$的用法:Shell变量展开与SQL正则锚点
2026/11/3 4:35:12 网站建设 项目流程

“mysql $的用法”这个词,我在搜索词记录里见过很多次。点进来的人大致分两类:一类是敲完一条mysql命令,终端甩出一行Access denied或者Unknown database,回头一看自己命令里有个$,搞不清它到底属于谁;另一类是刚接手别人写的运维脚本,里面满屏的"$DB_NAME"、-p"$PASS"、WHERE name = '$n',想弄明白这一堆$分别在什么层面生效。说实话,$这个东西在 MySQL 的语境里根本不只有一个身份,它同时可能是 shell 的提示符符号、shell 的变量前缀、SQL 语句里的普通字符,甚至是正则表达式的行尾锚点。这四个身份混在一起,才让“mysql $的用法”成了一个高频疑问。这篇就把 MySQL 基础用法里跟$相关的部分掰开揉碎讲清楚,从最外层 shell 怎么把命令喂给客户端,到里层 SQL 到底认不认这个字符,再到脚本化时怎么把这些坑绕过去。适合已经能敲SELECT但一写脚本就翻车的人,也适合刚接手一堆遗留脚本、想快速看懂别人在干嘛的朋友。

1.$在 MySQL 语境里的三种身份,先分清楚再谈用法

很多人在搜索框里敲下这个问题的时候,脑子里其实只有一个模糊的疑问:为什么我照着教程敲的命令跑不起来。要回答这个疑问,第一件事不是去看 SQL 语法,而是建立一个分层模型——你敲的每一条mysql命令,都要先经过 shell 解析,再交给 mysql 客户端解析,最后交给服务端解析。$在这三层里的待遇完全不同,混着看就必然混乱。

1.1 身份一:终端提示符里的$,跟 MySQL 一点关系都没有

打开一个普通用户的终端,最常见的提示符长这样:$。这个$是 shell 给普通用户用的提示符,root 用户通常是#。不少人第一次看教程,看到命令行写成$ mysql -u root -p,就以为$是命令的一部分,直接把它复制黏贴进终端,结果报command not found。这个误会极其普遍,尤其是在看一些老文档的时候。

进入 mysql 客户端之后,提示符会变成mysql>。这里又有一套自己的符号体系,值得记一下,因为读懂提示符能帮你判断是不是引号没闭合:

提示符含义
mysql>正常等待输入,语句还没结束
->上一条语句没写完,还在续行
'>单引号没闭合
">双引号没闭合
`>反引号没闭合
/*>块注释没闭合

我见过最典型的新手卡点,是敲了一条带单引号的 SQL,忘了收尾,结果后面敲什么都被吞进同一个字符串里,提示符一直停在'>,人还以为客户端卡死了。其实只要看一眼提示符就知道问题在哪。

提示符还能自定义。会话里直接敲prompt \u@\h [\d]>\就变了,\u是当前用户,\h是主机,\d是当前库名。想长期生效就写进配置文件的[mysql]段:

[mysql] prompt="\u@\h [\d]>\ "

改完之后,你随时能知道自己连的是哪台机器、哪个库,多实例环境下这个习惯能救命。我就有过在生产库上敲DROP TABLE的前一秒,靠提示符里的库名发现连错了环境的经历。

1.2 身份二:shell 的变量前缀,绝大多数报错都源于此

真正让$变成问题的,是 shell 的变量展开。$PASS、${DB}、$(date +%F)、$1、$?这些写法在 shell 里都有特殊含义,shell 会在把命令交给mysql之前,先完成替换。也就是说,mysql 客户端根本看不到你写的那串$PASS,它看到的是替换后的结果。

这里最坑的一点:未定义的变量在默认情况下会展开成空字符串,而且不报错。你写了-p"$PASS",如果PASS拼错了大小写,命令会变成-p"",客户端拿着空密码去连,服务端返回Access denied。报错信息指向的是密码错误,而真正的问题在拼写,这个坑能把人绕进去很久。

想让它报错而不是静默变空,可以在脚本开头加一行:

set -u

开启之后,引用未定义变量会直接终止脚本并提示unbound variable。配合set -e(出错即退出)和set -o pipefail(管道中任一环节失败即失败),三件套一起用,脚本的健壮性会上一个台阶。

1.3 身份三:SQL 语句内部的合法字符与正则锚点

抛开 shell 不谈,$在 MySQL 的 SQL 语法里本身是合法的。它可以出现在不加引号的标识符里,可以作为字符串字面量的一部分,在正则表达式里还是行尾锚点。但它不能用来声明变量——MySQL 不支持$var这种变量写法,那是别的数据库体系的习惯。用户变量是@var,系统变量是@@var,这两者在日常使用中要分清。

把这三层分开之后,再看任何一个带$的命令,你就能快速定位它属于哪一层。下面这条命令里就有三层同时出现:

mysql -u"$USER_NAME" -p"$PASS" "$DB" -e 'SELECT * FROM t WHERE code REGEXP "^A[0-9]+$"'

"$USER_NAME"、"$PASS"、"$DB"是 shell 层;'...'单引号包裹的内容 shell 不展开;最里头的^A[0-9]+$是 SQL 正则层的锚点,表示整串必须从 A 开头、以数字结尾。理解了这个分层,剩下的内容就都是细节了。

2. 连上数据库之前:密码和参数里的$会先被 shell 吃掉

连接这一环是$问题最集中的地方。原因很简单——密码通常都带特殊字符,而命令行参数又是 shell 最先处理的部分。这一章的目标是让你彻底搞清楚 shell 的引用规则,以及密码该怎么传。

2.1 单引号、双引号、反斜杠的展开差异实测

shell 里三种引用的行为差异,是必须先背下来的基本功:

引用形式$是否展开能否包含空格适用场景
不加引号是否(按空格拆词)基本不用
双引号"..."是是需要变量展开的普通参数
单引号'...'否是密码、SQL 正文、含$的字面量
反斜杠转义\$否视情况双引号内局部转义

记住一句话就够:要展开用双引号,不展开用单引号,混着来就转义。

还有一个隐藏的坑:反引号在双引号内部依然会被执行。下面这条命令看起来是给 SQL 里的字段名加引号,实际上 shell 会先把`id`当成命令执行:

# 危险写法 mysql -e "SELECT `id`, `name` FROM users"

如果系统里恰好有id这个命令,它会被执行,输出被拼进 SQL;如果没有,直接报command not found。正确做法是把 SQL 整体放进单引号:

mysql -e 'SELECT `id`, `name` FROM users'

或者用$()的反引号替代思路——但这条路上没有优雅解,老老实实用单引号最省事。如果 SQL 里既需要单引号又需要变量展开,那就用 heredoc,后面章节会讲。

2.2 密码里带$的正确写法与免密登录方案

mysql -p后面紧跟密码,中间不能有空格。写成mysql -p $PASS会被解析成“密码为空,$PASS是数据库名”,报Unknown database。这个错误提示看起来完全跟密码无关,实际原因在空格。

密码里带$时,正确写法是用单引号包住:

mysql -u appuser -p'p@ss$w0rd' mydb

注意-p和单引号之间没有空格。单引号让 shell 不做展开,客户端拿到的是完整字符串p@ss$w0rd。

如果密码里同时包含单引号,单引号方案就失效了。这时候可以退回到交互式输入,只写-p不带值,让客户端提示你输入:

mysql -u appuser -p mydb # 回车后输入密码,此时密码不经过 shell 解析

但脚本里没法交互,所以更彻底的做法是不在命令行写密码。mysql_config_editor是官方提供的小工具,它把连接信息加密存进用户目录下的.mylogin.cnf,之后用--login-path引用:

mysql_config_editor set --login-path=mydb-prod \ --host=127.0.0.1 --user=appuser --password mysql --login-path=mydb-prod mydb -e 'SELECT 1'

用mysql_config_editor print --all可以列出配置项,但密码本身显示不出来,这一点比明文写在脚本里强太多。需求前提是你能接受在本机存一份加密文件——共享机器上要谨慎。

还有一个偏门写法是环境变量MYSQL_PWD:

MYSQL_PWD='p@ss$w0rd' mysql -u appuser mydb -e 'SELECT 1'

单引号保护了$,功能上没问题。但这个变量在进程环境里可见,同机其他用户通过ps之类的手段可能读到,只建议在自己独占的开发机上用。

2.3 配置文件里写$为什么反而没事

很多人把连接参数写进~/.my.cnf,这里写password = p@ss$w0rd是完全安全的,因为配置文件不经过 shell 解析,$就是普通字符。

但配置文件有它自己的规则:#和;开头的行是注释,值里如果包含#或;,需要加双引号。所以密码里同时有$和#的时候,写成:

[client] user = appuser password = "p@ss$w0rd#2024"

这里加双引号是为了保护#,不是为了$。顺带一提,配置文件的权限要收紧到600,否则同机用户能直接读到明文密码:

chmod 600 ~/.my.cnf

我自己踩过的坑是:在开发机上一切正常,部署到另一台机器上就Access denied。查了很久才发现对方把密码写在~/.my.cnf里,但那台机器上有个同名文件属于别的用户,同时存在的时候读取顺序不确定。多文件冲突这种事,用mysql --print-defaults看一眼实际生效的参数最直接。

3. 语句内部:MySQL 到底认不认$这个字符

过了 shell 这一关,接下来要问的是:发给服务端的 SQL 里带$,会不会出问题。答案要分位置看。

3.1 标识符里可以有$,但反引号该加还得加

MySQL 对不加引号的标识符有明确的字符限制:字母、数字、_、$,且不能以数字开头。也就是说order$detail这样的表名、user$id这样的字段名,在不加引号的情况下是合法的。

那就意味着可以随便用吗?不是。不加引号的标识符不能包含空格,不能是保留字,也不能以数字开头。为了统一风格、减少意外,我的习惯是只要标识符里出现$,一律加反引号:

CREATE TABLE `order$detail` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `order$no` VARCHAR(32) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这里补充一个现实情况:$在标识符里出现,很多时候不是设计出来的,而是历史遗留。早年一些应用会把分库分表的后缀拼进表名,比如log$202401。接手这种库的时候,你的所有 SQL 都得加反引号,ORM 里的字段映射也要检查一遍,否则查出来的字段名对不上。

3.2 MySQL 没有$变量,别把别家的习惯带过来

这一点必须强调,因为从别的数据库体系转过来的人特别容易犯。MySQL 里没有$var这种变量语法:

场景MySQL 写法常见误写
会话变量SET @n = 1;SELECT @n;SET $n = 1
系统变量SELECT @@version;SELECT $version
存储过程局部变量DECLARE v_cnt INT;DECLARE $cnt INT
预处理占位符PREPARE s FROM 'SELECT ?';PREPARE s FROM 'SELECT $1'

别的体系里用$1、$2做位置参数,或者用v_前缀声明局部变量,搬到 MySQL 里全都不认。在 MySQL 里做动态 SQL,标准路子是用户变量加预处理语句:

SET @sql = CONCAT('SELECT * FROM `order$detail` WHERE `order$no` = ?'); SET @no = 'A2024001'; PREPARE stmt FROM @sql; EXECUTE stmt USING @no; DEALLOCATE PREPARE stmt;

占位符固定是?,不是$1。这条路上有个约束:预处理语句里能用的动态部分是有限的,表名、字段名可以用CONCAT拼进去,但LIMIT后面的值在早期版本里不支持占位符,8.0 之后才放宽。

3.3 字符串字面量里的$永远安全

如果把$放进单引号包裹的字符串里,它就是个普通字符,不会有任何解析问题:

SELECT '$' AS dollar_sign, 'price$100' AS sample, 'currency$' = 'currency$' AS is_equal;

所以真正要操心的从来不是“SQL 认不认$”,而是“shell 会不会提前把它换掉”。把这一层想明白,排查效率能提高一大截。

4. 正则里的^和$:锚定语义与 LIKE 的根本差异

$在 MySQL 里最“正统”的用途,其实是正则表达式的行尾锚点。^表示字符串开头,$表示字符串结尾,两者配合就是精确匹配。这块内容在基础用法里经常被一带而过,但实际项目里写错的人特别多。

4.1 LIKE 天生全串匹配,REGEXP 天生子串匹配

这是最容易混淆的一点。LIKE的语义是整串匹配,%代表任意长度字符,所以WHERE name LIKE 'abc'只匹配名字恰好等于abc的行。而REGEXP的语义是只要字符串中存在一个匹配片段就算命中,WHERE name REGEXP 'abc'会把abc123、xxabcxx全部捞出来。

要让REGEXP表现得像整串匹配,必须手动加锚点:

-- 命中 abc,不命中 abc123 SELECT * FROM t WHERE name REGEXP '^abc$'; -- 命中以 abc 开头的 SELECT * FROM t WHERE name REGEXP '^abc'; -- 命中以 abc 结尾的 SELECT * FROM t WHERE name REGEXP 'abc$';

我在做数据清洗时踩过一次:用REGEXP 'error'统计错误日志,结果把no_error_found也统计进去了,数字虚高一截。加上锚点才对。

4.2 8.0 的正则函数家族与大小写开关

MySQL 8.0 引入了 ICU 正则库和一整套函数,比老版本的REGEXP运算符好用很多:

函数作用示例
REGEXP_LIKE(expr, pat, mode)判断是否匹配REGEXP_LIKE(name,'^a','i')
REGEXP_REPLACE(expr, pat, repl)替换匹配部分REGEXP_REPLACE(code,'[^0-9]','')
REGEXP_SUBSTR(expr, pat)提取匹配片段REGEXP_SUBSTR(log,'[0-9]{3}')
REGEXP_INSTR(expr, pat)返回匹配位置REGEXP_INSTR(txt,'x')

REGEXP_LIKE的第三个参数可以组合,i是忽略大小写,c是区分大小写,m是多行模式(这个模式下^和$会匹配每一行的开头结尾,而不是整个字符串的首尾),n让.能匹配换行。

这里有个特别隐蔽的坑:当REGEXP_LIKE的第一个参数是二进制字符串时会直接报错。比如某个字段是VARBINARY,或者你的表达式里用了返回二进制串的函数,就会得到Character set 'binary' cannot be used in conjunction with 'regexp_like'。解决办法是先转字符集:

SELECT REGEXP_LIKE(CONVERT(bin_col USING utf8mb4), '^abc$');

这个报错信息不算直观,第一次遇到容易懵。

另外注意 8.0 里正则默认是否区分大小写,取决于参数的排序规则。为了行为确定,建议显式指定c或i,别依赖默认值。

4.3 用^...$做精确匹配时索引还能不能用

这是个必须讲清的性能问题。结论:REGEXP基本用不上索引。优化器不会把正则条件改写成范围扫描,所以只要WHERE里出现REGEXP,大概率就是全表扫或者全索引扫。

对比一下LIKE:

写法能否走索引
col LIKE 'abc%'能,走范围扫描
col LIKE '%abc'不能,左模糊
col LIKE '%abc%'不能
col REGEXP '^abc'一般不能
col = 'abc'能,等值查找

如果你的场景是“精确匹配”,那就别用REGEXP,直接用=或者IN。如果是“前缀匹配”,用LIKE 'abc%'。正则留给那些确实需要复杂模式的场景,比如校验格式、提取片段。

真要在正则上做索引加速,8.0 有个变通思路:建生成列 + 索引。

ALTER TABLE t ADD COLUMN code_prefix VARCHAR(8) GENERATED ALWAYS AS (LEFT(code, 8)) STORED, ADD INDEX idx_code_prefix (code_prefix);

查询时先让code_prefix = 'ABC12345'走索引把范围缩小,再用REGEXP做精细过滤。这种“先粗筛后精筛”的套路,在大表上效果非常明显,我实测过一个千万级表,从十几秒压到几十毫秒。

5. 把常用查询脚本化:shell 变量驱动 mysql 的四种写法对比

日常真正高频的场景,是把 SQL 写进 shell 脚本里定时跑。这时候$的两层含义会同时出现,写法选错了就是无穷无尽的排查。这里把四种常见写法摆在一起对比。

5.1-e直接拼字符串的适用边界

最简单的方式是-e:

mysql -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e "SELECT COUNT(*) FROM users WHERE status = 1"

这条命令没问题,因为 SQL 里没有需要 shell 保护的特殊字符。但只要 SQL 里出现下面任意一个东西,就得格外小心:

  • *:不加引号会被当成文件名通配符展开
  • ?:同样是通配符,匹配单个字符
  • `:反引号会被当命令执行
  • $:会被当变量展开
  • ":会提前结束双引号

其中*这个坑最隐蔽。SELECT * FROM t如果整个-e参数没加引号,shell 会尝试把*展开成当前目录下的所有文件名,SQL 直接变形。虽然加上双引号就没事,但双引号又打开了$和反引号的口子。

结论:-e只适合那种“SQL 写死在脚本里、不含任何动态值和特殊符号”的场景。一旦需要往里塞变量,就该换写法。

5.2 heredoc 加引号包裹的定界符,最省心的做法

heredoc 是我日常最推荐的写法。关键在于定界符要加引号:

DB_NAME="mydb" mysql -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" <<'SQL' SELECT `id`, `name` FROM `order$detail` WHERE `order$no` = 'A2024001' AND code REGEXP '^[A-Z]{2}[0-9]{6}$'; SQL

注意<<'SQL'里的单引号。加了引号,heredoc 内部的$、反引号、\全部原样传给 mysql,shell 不碰。这样你就可以放心地在 SQL 里写反引号字段名、写正则的$锚点,完全不用转义。

需要把 shell 变量传进 SQL 时,去掉引号即可,但这时候 SQL 里的$又会展开。折中方案是用占位符加PREPARE,或者先把变量赋给 MySQL 用户变量:

mysql -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" <<SQL SET @start_date = '$START_DATE'; SELECT COUNT(*) FROM orders WHERE created_at >= @start_date; SQL

这里$START_DATE是有意展开的,而 SQL 里其他位置的$得自己用\$转义。代码里最好加一行注释说明哪些$是故意展开的,方便后来人。

5.3-N -B拿干净结果做后续处理

脚本里取查询结果继续加工时,输出格式就很重要。默认输出带边框和表头,很难解析。几个参数组合起来能得到干净数据:

参数作用
-N不输出列名
-B批处理模式,字段用 tab 分隔
-r关闭转义,原样输出
-s静默模式,减少冗余提示
--default-character-set=utf8mb4避免中文乱码

典型用法:

count=$(mysql -N -B -s -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" \ -e 'SELECT COUNT(*) FROM orders WHERE status = 0') echo "待处理订单数: $count"

注意-B会对特殊字符做转义,比如换行会变成\n字面量。如果你的字段里有换行,记得加-r关闭转义,否则拿到的数据和期望不符。这个细节我在导出多行文本字段时栽过,数据里全是字面的\n字符串。

5.4 参数化思路:用PREPARE替掉手工拼接

如果 SQL 里的动态值来自外部输入,字符串拼接就有注入风险。稳妥做法是把值交给 MySQL 的预处理机制,让它自己处理引用:

mysql -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" <<SQL SET @keyword = '$KEYWORD'; SET @sql = 'SELECT id, name FROM users WHERE name LIKE CONCAT(''%'', ?, ''%'')'; PREPARE s FROM @sql; EXECUTE s USING @keyword; DEALLOCATE PREPARE s; SQL

这里最容易错的是 SQL 里嵌套的单引号——因为在 heredoc 里,''就是两个单引号,表示字符串里的一个引号。这种写法可读性一般,但比手工拼WHERE name = '$KEYWORD'安全得多。

顺带说一句,SQL 里有个QUOTE()函数,可以给你传入的字符串自动加引号并转义:

SELECT QUOTE('it''s ok'); -- 返回 'it''s ok'

在必须拼接的场景下,用它做最后一道保险。

6. 一次完整的排错链路:连接突然被拒,最后发现是$作怪

讲原理容易,实战排查才见真章。下面这个案例我印象很深,完整的链路值得复现一遍,因为它把前面几个知识点全串起来了。

6.1 从报错信息反推是哪一层出的问题

现象是这样的:某开发者写了个定时任务脚本,每天凌晨跑一次数据归档,之前一直正常,改了密码之后就再没成功过。手动执行脚本,输出:

ERROR 1045 (28000): Access denied for user 'appuser'@'localhost' (using password: YES)

using password: YES这个信息很关键,说明客户端确实把密码发出去了,只是密码不对。如果显示using password: NO,那说明压根没传密码参数。

手敲mysql -u appuser -p然后交互输入同样的密码,能正常登进去。这就排除了“密码本身就错”和“账号权限被改”这两种可能。问题被锁死在脚本这一侧。

6.2 用set -x和printf '%q'把展开结果打出来

打开脚本一看,配置文件长这样:

# env.sh DB_USER=appuser DB_PASS=p@ss$w0rd DB_NAME=mydb

DB_PASS=p@ss$w0rd这一行没有加引号,$w0rd被当成变量名,而它从未被定义,于是展开成空字符串。最终DB_PASS的值变成了p@ss——少了后半截,登录自然失败。

验证方法有两个,配合使用效果最好。

第一个是临时打开追踪:

bash -x archive.sh

输出里会看到+ mysql -uappuser -pp@ss mydb,密码只剩前半截,一目了然。

第二个是用printf '%q',它会把变量的实际值按 shell 可复用的形式打印出来,不可见字符和特殊字符都会显式转义:

source env.sh printf '%q\n' "$DB_PASS" # 输出:p@ss

对比一下正常情况:

DB_PASS='p@ss$w0rd' printf '%q\n' "$DB_PASS" # 输出:p@ss\$w0rd

两个输出一对比,问题无处遁形。printf '%q'这个技巧我强烈建议记下来,比echo靠谱太多——echo遇到空变量什么都不打印,你甚至看不出变量是空的还是没定义。

6.3 修复后如何验证不再复发

修复只需要一行:给值加单引号。

DB_PASS='p@ss$w0rd'

但光修好不算完,还得防止下次再犯。我的做法是在脚本头部加三行:

set -euo pipefail

其中-u就是关键。加了它,再遇到$w0rd这种未定义变量,脚本会在那一行直接退出并报错:

env.sh: line 3: w0rd: unbound variable

报错位置精确到行号,比默默连不上数据库强一万倍。

再进一步,把所有含特殊字符的配置值统一用单引号包裹,并在文件头写一条注释说明这个约定:

# 约定:所有变量值一律用单引号包裹,避免 shell 展开 DB_USER='appuser' DB_PASS='p@ss$w0rd' DB_NAME='mydb'

最后加一个自检环节,让脚本在真正干活之前先确认能连上:

if ! mysql --login-path=mydb-prod -e 'SELECT 1' >/dev/null 2>&1; then echo "数据库连接失败,终止执行" >&2 exit 1 fi

这个前置检查花不了几毫秒,但能让问题在第一时间暴露,而不是等到半夜归档失败了第二天才发现。

还有一个同类坑值得提前防:密码里含\的时候,双引号包裹会触发转义,密码被改写;含!的时候,在交互式 bash 里可能触发历史扩展。统一用单引号,这些麻烦全部消失。

7. 回到基础用法本身:日常高频操作的落地清单

把$这层窗户纸捅破之后,剩下的事情就顺畅多了。最后这一部分回到 MySQL 基础用法本身,挑几个日常复用率最高、但又最容易在细节上栽跟头的点讲。

7.1 建库建表时最容易忽略的几个默认值

建库的时候如果不指定字符集,会跟着服务端默认值走。不同环境默认值可能不一样,5.7 和 8.0 的默认字符集就不同。所以我一律显式写:

CREATE DATABASE IF NOT EXISTS mydb DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_0900_ai_ci;

utf8mb4而不是utf8,这一点现在基本是共识了——早期的utf8只支持三字节,存 emoji 会截断甚至报错。排序规则utf8mb4_0900_ai_ci是 8.0 的默认值,ai表示重音不敏感,ci表示大小写不敏感。如果你的业务需要区分大小写(比如订单号、优惠码),记得改成_as_cs结尾的排序规则,或者在建表时单独给字段指定。

建表时有几个细节我每次都检查:

CREATE TABLE `orders` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL COMMENT '业务单号,区分大小写', `amount` DECIMAL(12,2) NOT NULL DEFAULT 0.00, `status` TINYINT UNSIGNED NOT NULL DEFAULT 0, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_status_created` (`status`, `created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

金额用DECIMAL不用FLOAT,这个是老生常谈但仍有人踩。主键用BIGINT UNSIGNED而不是INT,考虑到增长空间。时间字段用DATETIME配合默认值和ON UPDATE,省掉应用层维护时间戳的代码。

需要区分大小写的order_no,可以单独指定排序规则:

`order_no` VARCHAR(32) COLLATE utf8mb4_0900_as_cs NOT NULL

如果不指定,唯一索引会把ABC和abc判为重复,业务上可能就是个事故。

7.2 查询与索引的最小可用套路

写完SELECT之后,养成一个习惯:在前面加EXPLAIN看执行计划。重点看三列:

列关注点
typeALL是全表扫,ref/range/eq_ref相对好
key实际用了哪个索引,NULL说明没走索引
rows预估扫描行数,越大越要警惕
Extra出现Using filesort、Using temporary要留意

几个高频优化点:

联合索引要遵守最左前缀。建了(status, created_at),那么WHERE status = 1能用,WHERE status = 1 AND created_at > '2024-01-01'也能用,但单独WHERE created_at > '...'用不上。

不要在索引列上做函数运算。WHERE DATE(created_at) = '2024-01-01'会导致索引失效,改成范围查询:

WHERE created_at >= '2024-01-01 00:00:00' AND created_at < '2024-01-02 00:00:00'

这个改写的收益在小表上看不出来,在百万级表上是从秒级到毫秒级的差别。

分页深翻页要避免大OFFSET。LIMIT 1000000, 20会让数据库扫描一百万行再丢掉,改成基于游标:

SELECT id, order_no FROM orders WHERE id > 1000000 ORDER BY id LIMIT 20;

前提是排序字段有索引且值唯一,否则会漏数据。

7.3 备份、恢复和日常巡检脚本骨架

备份这件事,mysqldump是最基础的方案,适合中小体量。几个参数值得配好:

mysqldump \ --single-transaction \ --quick \ --default-character-set=utf8mb4 \ --routines --triggers --events \ -u"$DB_USER" -p"$DB_PASS" \ "$DB_NAME" > "backup_$(date +%F).sql"

--single-transaction让 InnoDB 表在一个一致性快照里导出,不锁表;--quick逐行读取,避免把整表塞进内存。注意这两个参数对 MyISAM 表不起作用,如果库里还混着老引擎的表,备份期间会有锁。

恢复就简单了:

mysql -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" < backup_2024-01-01.sql

日常巡检我习惯跑几条固定查询,写成脚本每天早上看一眼:

-- 连接数与最大连接数 SHOW STATUS LIKE 'Threads_connected'; SHOW VARIABLES LIKE 'max_connections'; -- 当前正在跑的慢查询 SELECT id, user, time, LEFT(info, 80) AS sql_head FROM information_schema.processlist WHERE command <> 'Sleep' AND time > 5 ORDER BY time DESC; -- 表体积 Top 10 SELECT table_name, ROUND((data_length + index_length) / 1024 / 1024, 2) AS mb FROM information_schema.tables WHERE table_schema = 'mydb' ORDER BY data_length + index_length DESC LIMIT 10;

这几条查下来,基本能判断出当天有没有异常。连接数接近上限就说明有连接泄漏,慢查询扎堆就说明某个 SQL 或索引出了问题。

写这些脚本的时候,前面讲的$规则就派上用场了:所有变量加单引号包裹,SQL 正文用 heredoc 加引号定界符,结果取用加-N -B -s,再加个set -euo pipefail兜底。这么一套下来,脚本稳定性能提升一大截。

我个人在实际操作中的体会是,$这类符号引发的问题,难的不是修复,而是定位。修复往往就是加个引号的事,但从“数据库连不上”到“某个变量没加引号”之间隔着好几层。所以与其事后排查,不如事前把写法固定下来——配置值一律单引号,SQL 一律 heredoc 加引号定界符,脚本一律开set -u。这三条约定执行下去,同类问题基本就绝迹了。另外那个printf '%q'的小技巧,真的值得贴在显示器边上,它救我的次数比任何调试工具都多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询