Skip to content

MySQL 速查表

流行的开源关系型数据库管理系统。

01

入门

连接到 MySQL

mysql 客户端是 MySQL 的标准 CLI 工具。始终使用 -p(后面不加空格)以便安全地提示输入密码,而不是暴露在 shell 历史记录中。-h 设置主机,-P(大写)设置端口。\s 打印连接状态。使用 -e 在脚本中执行一次性查询。

mysql
# connect to local server (prompt for password)
mysql -u root -p

# connect to a remote host on a custom port
mysql -h 192.168.1.100 -P 3307 -u admin -p

# connect directly to a specific database
mysql -u root -p mydb

# non-interactive: run a query and exit
mysql -u root -p -e "SELECT VERSION();"

# show server status inside the client
\s
SELECT VERSION(), CURRENT_USER, DATABASE();

数据库管理

使用 utf8mb4(而非 utf8)以支持完整的 Unicode 包括 emoji——MySQL 的 utf8 是 3 字节子集,无法存储所有字符。utf8mb4_unicode_ci 是推荐的排序规则,可正确排序。IF EXISTS/IF NOT EXISTS 可防止脚本报错。DROP DATABASE 会立即删除所有表和数据。

mysql
# list all databases
SHOW DATABASES;

# create a database with charset and collation
CREATE DATABASE mydb
  CHARACTER SET utf8mb4
  COLLATE utf8mb4_unicode_ci;

# switch to a database
USE mydb;

# show the current database
SELECT DATABASE();

# drop a database (irreversible)
DROP DATABASE IF EXISTS mydb;

# alter a database's charset
ALTER DATABASE mydb CHARACTER SET utf8mb4;

显示与描述对象

SHOW 命令是 MySQL 特有的内省工具。DESCRIBE 提供快速列概览;SHOW CREATE TABLE 提供可复用的完整 DDL。在命令后加 \G 代替 ; 可垂直输出——对于 SHOW CREATE TABLE 等宽行结果更易读。

mysql
# list tables in the current database
SHOW TABLES;

# show table structure (columns, types, keys)
DESCRIBE users;
# equivalent
SHOW COLUMNS FROM users;

# show create statement for a table
SHOW CREATE TABLE users\G

# list indexes on a table
SHOW INDEX FROM users;

# show stored procedures / functions
SHOW PROCEDURE STATUS WHERE Db = 'mydb';
SHOW FUNCTION STATUS WHERE Db = 'mydb';

# list triggers
SHOW TRIGGERS\G

服务器状态与变量

MySQL 变量有 SESSION(当前连接)和 GLOBAL 两种作用域。SET GLOBAL 修改运行中的服务器但重启后失效——应将设置持久化到 my.cnf 中。@@var 读取变量值。SHOW STATUS 暴露运行时计数器(连接数、运行时间、吞吐量),可用于监控。

mysql
# view a system variable
SHOW VARIABLES LIKE 'max_connections';
SHOW VARIABLES LIKE 'version%';

# view session vs global variable
SELECT @@session.sql_mode, @@global.sql_mode;

# set a variable for the current session
SET sql_mode = 'STRICT_TRANS_TABLES';
SET @@session.foreign_key_checks = 0;

# set globally (needs SUPER/privilege, persists until restart)
SET GLOBAL max_connections = 200;

# show server status counters
SHOW STATUS LIKE 'Threads%';
SHOW STATUS LIKE 'Uptime';

注释与语句基础

MySQL 支持三种注释风格:--(需要尾部空格)、/* */ 块注释和 # 到行尾。/*! ... */ 是特殊的可执行注释——其中的代码仅在 MySQL 上运行,被其他 SQL 引擎忽略,适用于可移植的 schema 文件。\G 执行并以垂直方式显示结果。

mysql
-- single-line comment (note the space after --)
SELECT 1; -- inline comment

/* multi-line
   comment block */
SELECT 2;

# hash-style comment (MySQL-specific, to end of line)
SELECT 3;

# MySQL executable comments: run only on MySQL
SELECT 1 /*!50100 , 2 */;  /* the part runs on MySQL >= 5.1 */

# statements end with semicolon; \G ends and prints vertically
SELECT * FROM users\G

配置文件 (my.cnf)

my.cnf(Linux)/ my.ini(Windows)保存持久化的服务器和客户端设置,按节组织:[mysqld] 为服务器,[client]/[mysql] 为客户端。innodb_buffer_pool_size 是 InnoDB 最重要的调优参数(通常为内存的 50-75%)。编辑后需重启服务器。用 mysql --help 或 SHOW VARIABLES 验证配置。

mysql
# /etc/my.cnf or ~/.my.cnf  (Linux/macOS)
# C:\ProgramData\MySQL\MySQL Server 8.0\my.ini  (Windows)

[mysqld]
port            = 3306
datadir         = /var/lib/mysql
max_connections = 200
character-set-server = utf8mb4
collation-server     = utf8mb4_unicode_ci
innodb_buffer_pool_size = 2G
slow_query_log  = 1
long_query_time = 2

[client]
default-character-set = utf8mb4

[mysql]
prompt = \u@\h [\d]>\_
02

DDL 操作

创建表

CREATE TABLE 定义列、类型、约束和表选项。InnoDB 是默认且推荐的存储引擎(支持事务、行级锁、外键)。AUTO_INCREMENT 使用 BIGINT UNSIGNED 可避免溢出。DECIMAL(p,s) 适合精确存储货币。ENUM 根据固定列表验证值。TIMESTAMP DEFAULT CURRENT_TIMESTAMP 在插入时自动填充。

mysql
CREATE TABLE users (
  id          BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  username    VARCHAR(50)  NOT NULL UNIQUE,
  email       VARCHAR(255) NOT NULL,
  birth_date  DATE         NULL,
  status      ENUM('active','inactive','banned') NOT NULL DEFAULT 'active',
  balance     DECIMAL(10,2) NOT NULL DEFAULT 0.00,
  created_at  TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

修改表

ALTER TABLE 无需重建数据即可演进 schema。ADD COLUMN 配合 AFTER 可将列放在指定位置(列顺序在 MySQL 中仅影响显示)。MODIFY 修改类型/默认值;RENAME COLUMN(8.0+)比 CHANGE 更简洁。大型 ALTER 操作可能重建并锁定表——大表请使用 pt-online-schema-change 或在线 DDL。RENAME TABLE 是原子操作。

mysql
# add a column
ALTER TABLE users
  ADD COLUMN phone VARCHAR(20) NULL AFTER email;

# add multiple columns
ALTER TABLE users
  ADD COLUMN first_name VARCHAR(50) NULL,
  ADD COLUMN last_name  VARCHAR(50) NULL;

# modify a column type
ALTER TABLE users
  MODIFY COLUMN phone VARCHAR(30) NOT NULL;

# rename a column (MySQL 8+ preserves data)
ALTER TABLE users
  RENAME COLUMN phone TO phone_number;

# rename a table
RENAME TABLE users TO members;
ALTER TABLE users RENAME TO members;

删除与清空表

DROP TABLE 完全删除表;TRUNCATE 清空数据但保留结构并将 AUTO_INCREMENT 重置为起始值。TRUNCATE 比 DELETE 快得多,因为它跳过了逐行删除和日志记录,但它是 DDL(无法回滚,不触发触发器)。清空被其他表引用的表时需禁用 foreign_key_checks。

mysql
# drop a table (removes structure and data)
DROP TABLE IF EXISTS old_logs;

# drop multiple tables
DROP TABLE IF EXISTS temp1, temp2;

# truncate: empty the table, keep structure, reset AUTO_INCREMENT
TRUNCATE TABLE session_data;

# truncate cannot be rolled back in some engines
# (InnoDB: TRUNCATE is DDL, implicitly commits)
SET foreign_key_checks = 0;
TRUNCATE TABLE child_table;
SET foreign_key_checks = 1;

AUTO_INCREMENT

AUTO_INCREMENT 为主键生成连续数字——每张表只能有一个。LAST_INSERT_ID() 返回当前会话最近一次 INSERT 生成的 id(按连接隔离,并发安全)。删除、回滚或显式指定值插入后可能出现间隔。重置请使用 ALTER TABLE ... AUTO_INCREMENT = n。

mysql
CREATE TABLE orders (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  amount DECIMAL(10,2)
);

# set the next auto-increment value
ALTER TABLE orders AUTO_INCREMENT = 1000;

# insert without specifying id
INSERT INTO orders (amount) VALUES (99.50);

# get the last inserted id
SELECT LAST_INSERT_ID();

# show current AUTO_INCREMENT value
SHOW TABLE STATUS LIKE 'orders'\G

表约束

约束在数据库层面强制数据完整性。InnoDB 支持主键、UNIQUE、NOT NULL、CHECK(8.0.16 起强制执行)和外键。FOREIGN KEY ... ON DELETE CASCADE 在父行删除时级联删除子行;SET NULL 将外键置空;RESTRICT/NO ACTION 阻止删除。显式命名约束便于管理。外键要求两侧列都有索引。

mysql
CREATE TABLE orders (
  id           INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  user_id      INT UNSIGNED NOT NULL,
  total        DECIMAL(10,2) NOT NULL,
  status       VARCHAR(20) NOT NULL,

  # unique constraint
  CONSTRAINT uq_order_no UNIQUE (user_id, status),

  # check constraint (MySQL 8.0.16+ enforced)
  CONSTRAINT chk_total CHECK (total >= 0),

  # foreign key with cascading actions
  CONSTRAINT fk_orders_user
    FOREIGN KEY (user_id) REFERENCES users(id)
    ON DELETE CASCADE
    ON UPDATE RESTRICT
) ENGINE=InnoDB;

# add a constraint later
ALTER TABLE orders
  ADD CONSTRAINT chk_status CHECK (status IN ('paid','shipped','cancelled'));

临时表

TEMPORARY 表仅存在于当前会话中,在连接关闭时自动删除。它可以遮蔽同名真实表,适用于安全重构或暂存数据。两个会话可以创建同名临时表而互不冲突。它们对其他连接不可见,默认不写入 binlog。

mysql
# session-scoped, auto-dropped on disconnect
CREATE TEMPORARY TABLE tmp_active
  SELECT id, username FROM users WHERE status = 'active';

# identical name shadowing a real table
CREATE TEMPORARY TABLE users
  (id INT, name VARCHAR(50));

# only this session sees the temp table
SELECT * FROM users;

# explicit cleanup
DROP TEMPORARY TABLE IF EXISTS tmp_active;
03

数据类型

数值类型

货币等需要精确性的场景使用 DECIMAL——FLOAT/DOUBLE 是近似值,会累积舍入误差。UNSIGNED 使正数范围翻倍但不允许负值。显示宽度(如 INT(11))在 MySQL 8.0 中已弃用并被忽略——它从未限制存储范围。AUTO_INCREMENT 建议用 BIGINT UNSIGNED 以避免溢出。SERIAL 是代理键的便捷别名。

mysql
# integers (with optional UNSIGNED)
TINYINT        -- 1 byte, -128..127 or 0..255 (UNSIGNED)
SMALLINT       -- 2 bytes
MEDIUMINT      -- 3 bytes
INT            -- 4 bytes
BIGINT         -- 8 bytes (use for large AUTO_INCREMENT)

# fixed-point (exact, for money)
DECIMAL(10,2)  -- 10 digits total, 2 after decimal
NUMERIC(8,4)

# floating-point (approximate)
FLOAT          -- 4 bytes
DOUBLE         -- 8 bytes

# other
BIT(8)         -- bit-field
SERIAL         -- alias for BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE

字符串与文本类型

VARCHAR 几乎总是优于 CHAR(会用空格右填充),除非是短固定长度代码。MySQL 的 utf8mb4 每个字符最多 4 字节——VARCHAR(255) 列最多需要 1020 字节加长度,这关系到索引键长度限制(InnoDB 为 3072 字节)。TEXT/BLOB 系列将大数据离页存储;它们不能有默认值,建索引需指定前缀长度。

mysql
# fixed vs variable length
CHAR(10)       -- fixed 10 chars, padded with spaces
VARCHAR(255)   -- variable, up to 255 chars (stores actual length)

# large text
TINYTEXT       -- up to 255 bytes
TEXT           -- up to 64 KB
MEDIUMTEXT     -- up to 16 MB
LONGTEXT       -- up to 4 GB

# binary counterparts
BINARY(16), VARBINARY(255), BLOB, MEDIUMBLOB, LONGBLOB

# national character set
NATIONAL VARCHAR(100)  -- same as VARCHAR with utf8mb4

# VARCHAR with charset counts characters, not bytes
VARCHAR(100) CHARACTER SET utf8mb4

日期与时间类型

DATETIME 存储字面日期/时间,不感知时区;TIMESTAMP 存储 UTC 并在显示时转换为会话 time_zone,更适合跨时区应用——但在 32 位构建上有 2038 年限制。使用 DATETIME(6)/TIMESTAMP(6) 获取微秒精度。自 MySQL 5.6.5 起 CURRENT_TIMESTAMP 可作为两者的默认值。用 SET time_zone = '+08:00'; 为每个连接设置时区。

mysql
DATE            -- 'YYYY-MM-DD', range 1000-01-01 .. 9999-12-31
TIME            -- 'HH:MM:SS', can be negative for elapsed time
DATETIME        -- 'YYYY-MM-DD HH:MM:SS', 8 bytes, no timezone
TIMESTAMP       -- stored as UTC, converted to session timezone, 4 bytes
YEAR            -- YEAR(4) e.g. 2024

# DATETIME with fractional seconds and auto-default
CREATE TABLE events (
  ts DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3)
                 ON UPDATE CURRENT_TIMESTAMP(3)
);

# TIMESTAMP range: 1970-01-01 .. 2038-01-19 (32-bit limit)
# MySQL 8.0.28+ extends TIMESTAMP to 24 bits of range

ENUM 与 SET 类型

ENUM 紧凑地存储固定列表中的一个字符串值(1-2 字节),在严格模式下拒绝未知值。SET 以位掩码形式存储最多 64 个成员的任意组合。两者修改 schema 成本较高(添加/重排成员需要 ALTER TABLE)且查询不便——许多团队更倾向于查找表或 VARCHAR 配合 CHECK。FIND_IN_SET 有助于查询 SET 列。

mysql
# ENUM: exactly one value from a list
CREATE TABLE shirts (
  size ENUM('S','M','L','XL') NOT NULL DEFAULT 'M',
  color ENUM('red','green','blue') NULL
);

# stored as integer index (1-based), space-efficient
INSERT INTO shirts (size) VALUES ('L'), ('XL');

# SET: zero or more values from a list (bit flags)
CREATE TABLE posts (
  tags SET('news','tech','sports','fun') NOT NULL DEFAULT ''
);

INSERT INTO posts (tags) VALUES ('tech,fun');

# query SET members with FIND_IN_SET
SELECT * FROM posts WHERE FIND_IN_SET('tech', tags);

JSON 类型

JSON 类型(MySQL 5.7.8+)以二进制格式存储 JSON,写入时验证并支持快速成员访问。它优于在 TEXT 列中存储 JSON,因为可以在其内部建索引和查询。使用 JSON_OBJECT()、JSON_ARRAY() 和 JSON_MERGE_PATCH() 构建值。对于频繁查询的几个键,将其提取到生成列并建索引以获得最佳性能。

mysql
CREATE TABLE products (
  id    INT PRIMARY KEY,
  name  VARCHAR(100),
  attrs JSON NOT NULL
);

# insert JSON literals
INSERT INTO products (id, name, attrs) VALUES
  (1, 'Widget', '{"color":"red","size":42,"tags":["new","sale"]}'),
  (2, 'Gadget', '{"color":"blue","in_stock":true}');

# JSON values are validated on insert and stored binary
SELECT attrs FROM products WHERE id = 1;

# pretty-print JSON
SELECT JSON_PRETTY(attrs) FROM products WHERE id = 1;

BLOB 与二进制类型

BLOB 存储任意二进制数据(图片、文件)但查询不便且不能有默认值。对于大文件,建议在数据库中存储路径,文件存放在磁盘/对象存储上。UUID 存储为 BINARY(16)(16 字节)而非 CHAR(36)(36 字节)以提高索引效率——MySQL 8.0 新增 UUID_TO_BIN()/BIN_TO_UUID(),可选项交换以利于索引。

mysql
# binary large objects
TINYBLOB   -- up to 255 bytes
BLOB       -- up to 64 KB
MEDIUMBLOB -- up to 16 MB
LONGBLOB   -- up to 4 GB

# fixed-size binary (e.g. hashes, UUIDs)
BINARY(16)    -- fixed 16 bytes, right-padded with \0
VARBINARY(255)

# store a UUID as binary for compactness
CREATE TABLE sessions (
  id BINARY(16) PRIMARY KEY,
  data VARBINARY(1000)
);
INSERT INTO sessions (id, data) VALUES (UUID_TO_BIN(UUID()), '...');

# retrieve as text
SELECT BIN_TO_UUID(id) FROM sessions;
04

DML(INSERT / UPDATE / DELETE)

INSERT

多行 INSERT 比反复单行插入快得多,因为它摊销了解析、网络往返和索引更新的开销。INSERT ... SELECT 在表间复制数据。SET 语法是 MySQL 特有的,适合单行插入。始终显式列出目标列,使语句能适应 schema 变更。使用 INSERT IGNORE 或 ON DUPLICATE KEY UPDATE 实现幂等插入。

mysql
# single row
INSERT INTO users (username, email)
VALUES ('alice', '[email protected]');

# multiple rows in one statement
INSERT INTO users (username, email) VALUES
  ('bob', '[email protected]'),
  ('carol', '[email protected]'),
  ('dave', '[email protected]');

# insert from a SELECT
INSERT INTO archive_users (username, email)
SELECT username, email FROM users WHERE status = 'inactive';

# insert with column-order-free syntax
INSERT INTO users SET username='eve', email='[email protected]';

INSERT ... ON DUPLICATE KEY UPDATE(Upsert)

ON DUPLICATE KEY UPDATE 实现 upsert——当 UNIQUE/PRIMARY KEY 冲突时,MySQL 更新现有行而非报错。VALUES(col) 引用本应插入的值;MySQL 8.0.19+ 弃用此写法,改用行别名(AS new)。这比先 SELECT 再 INSERT/UPDATE 更高效,因为它是原子的。INSERT IGNORE 则静默跳过冲突。

mysql
CREATE TABLE counters (
  name VARCHAR(50) PRIMARY KEY,
  hits INT NOT NULL DEFAULT 0
);

# upsert: insert or update on key conflict
INSERT INTO counters (name, hits)
VALUES ('home', 1)
ON DUPLICATE KEY UPDATE hits = hits + 1;

# reference the proposed values with VALUES()
INSERT INTO counters (name, hits) VALUES ('about', 5)
ON DUPLICATE KEY UPDATE hits = VALUES(hits) + 1;

# MySQL 8.0.20+: use an alias for the row being inserted
INSERT INTO counters (name, hits) VALUES ('home', 1) AS new
ON DUPLICATE KEY UPDATE hits = counters.hits + new.hits;

UPDATE

始终包含 WHERE 子句——不带 WHERE 的 UPDATE 会修改每一行。MySQL 支持带 JOIN 的多表 UPDATE,适用于反规范化计算值。ORDER BY + LIMIT 可实现安全的批量更新,避免大表上长时间锁表。在 safe-updates 模式下,客户端会拒绝 WHERE 中没有键的 UPDATE/DELETE,防止误批量修改。

mysql
# basic update with WHERE (always include one!)
UPDATE users
SET status = 'banned', email = NULL
WHERE id = 42;

# update with expression
UPDATE products
SET price = price * 1.10
WHERE category = 'electronics';

# multi-table update with JOIN
UPDATE users u
JOIN orders o ON o.user_id = u.id
SET u.total_spent = u.total_spent + o.amount
WHERE o.paid = 1;

# limit and order (useful for batched updates)
UPDATE logs SET archived = 1
WHERE archived = 0
ORDER BY created_at ASC
LIMIT 1000;

DELETE

DELETE 逐行删除,是事务性的(可回滚)且触发触发器。多表 DELETE 可在一条语句中删除多个表的数据。清空整表用 TRUNCATE 更快且重置 AUTO_INCREMENT,但不可回滚。QUICK 修饰符(MyISAM)跳过索引叶更新——InnoDB 基本不需要。始终使用 WHERE 子句。

mysql
# delete specific rows
DELETE FROM users WHERE status = 'banned' AND last_login < '2020-01-01';

# delete with LIMIT (batched)
DELETE FROM logs WHERE created_at < '2023-01-01' LIMIT 5000;

# multi-table delete with JOIN
DELETE u, o
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.id = 99;

# delete all rows (keeps table & AUTO_INCREMENT, unlike TRUNCATE)
DELETE FROM temp_data;

# quick delete that doesn't return row count
DELETE QUICK FROM logs WHERE year = 2020;

REPLACE

REPLACE 是 MySQL 特有的:如果存在相同 PK/UNIQUE 键的行,先删除再插入新行;否则直接插入。比 upsert 简单但有副作用——它触发 DELETE 然后 INSERT 触发器(而非 UPDATE),重置 AUTO_INCREMENT,并将未提供的列重置为默认值。大多数场景建议优先使用 INSERT ... ON DUPLICATE KEY UPDATE。

mysql
# REPLACE deletes then inserts on key conflict
REPLACE INTO users (id, username, email)
VALUES (1, 'alice', '[email protected]');

# REPLACE ... SET form
REPLACE INTO users SET id = 1, username = 'alice', email = '[email protected]';

# REPLACE from SELECT
REPLACE INTO daily_stats (day, visits)
SELECT CURDATE(), COUNT(*) FROM visits_log;

# caveat: REPLACE fires DELETE + INSERT triggers,
# not UPDATE, and resets columns not listed

LOAD DATA INFILE

LOAD DATA INFILE 是批量加载 CSV 最快的方式——比 INSERT 语句快 20 倍,因为它绕过了 SQL 解析。服务器从安全目录(secure_file_priv)读取文件;LOCAL 让客户端发送文件但默认因安全原因禁用。使用 @var 捕获列再 SET 在加载时转换值。SELECT ... INTO OUTFILE 是对称的导出操作。

mysql
# fast bulk import from a CSV file
LOAD DATA INFILE '/var/lib/mysql-files/users.csv'
INTO TABLE users
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 LINES
(username, email, @created)
SET created_at = STR_TO_DATE(@created, '%Y-%m-%d %H:%i:%s');

# LOCAL reads from the client machine (needs --local-infile)
LOAD DATA LOCAL INFILE 'C:/data/users.csv'
INTO TABLE users
FIELDS TERMINATED BY ','
LINES TERMINATED BY '\r\n'
IGNORE 1 LINES (username, email);

# export counterpart
SELECT * FROM users INTO OUTFILE '/tmp/users.csv'
FIELDS TERMINATED BY ',' ENCLOSED BY '"'
LINES TERMINATED BY '\n';
05

SELECT 查询

基本 SELECT

生产环境避免 SELECT *——它传输不必要的数据,在增删列时出错,并阻止优化器使用覆盖索引。只查询需要的列。LIMIT 配合 OFFSET 实现分页但大偏移量时很慢(仍需扫描跳过的行);键集分页(WHERE id > last_id LIMIT n)扩展性更好。

mysql
# select all columns
SELECT * FROM users;

# select specific columns (preferred)
SELECT id, username, email FROM users;

# column aliases
SELECT username AS name, email AS contact FROM users;

# constants and expressions
SELECT 1 + 1 AS sum, NOW() AS now, 'hello' AS greeting;

# limit rows and skip (pagination)
SELECT id, username FROM users
ORDER BY id DESC
LIMIT 10 OFFSET 20;

# MySQL shorthand: LIMIT offset, count
SELECT id, username FROM users LIMIT 20, 10;

WHERE 与运算符

与 NULL 的比较结果为 NULL(视为 false),所以始终使用 IS NULL / IS NOT NULL。IN/NOT IN 列表中的 NULL 可能产生意外的空结果——改用 NOT EXISTS 或相关子查询。BETWEEN 两端都包含。MySQL 先评估 AND 后评估 OR,混合条件需加括号。为最佳性能,将最具选择性的条件放前面,并确保索引列在运算符左侧。

mysql
# comparison operators
SELECT * FROM products WHERE price < 100;
SELECT * FROM products WHERE price BETWEEN 50 AND 150;
SELECT * FROM users WHERE id IN (1, 5, 9);

# logical operators
SELECT * FROM users
WHERE status = 'active' AND (age >= 18 OR verified = 1);

# NULL checks (never use = with NULL)
SELECT * FROM users WHERE email IS NULL;
SELECT * FROM users WHERE email IS NOT NULL;

# BETWEEN is inclusive on both ends
SELECT * FROM orders WHERE created_at
  BETWEEN '2024-01-01 00:00:00' AND '2024-01-31 23:59:59';

# NOT IN with NULL traps: NULL poisons the result

ORDER BY 与 LIMIT

ORDER BY 排序结果;多个键从左到右依次应用。FIELD() 支持按枚举列表自定义排序。对于大结果集,除非有索引支持,ORDER BY ... LIMIT 仍需扫描并排序整个集合——在 ORDER BY 列上添加索引。窗口函数模式(ROW_NUMBER OVER PARTITION BY)是 MySQL 8.0+ 中每组取 Top-N 的标准方案。

mysql
# ascending (default) and descending
SELECT * FROM users ORDER BY created_at;
SELECT * FROM users ORDER BY created_at DESC, username ASC;

# order by expression
SELECT product, price * stock AS value
FROM inventory
ORDER BY value DESC;

# order by FIELD() for custom ordering
SELECT * FROM tasks
ORDER BY FIELD(priority, 'high', 'medium', 'low');

# stable top-N per group via window function (8.0+)
SELECT * FROM (
  SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY id DESC) rn
  FROM orders
) t WHERE rn <= 3;

DISTINCT 与去重

DISTINCT 折叠相同行;多列时对组合去重。COUNT(DISTINCT col) 计算唯一的非空值。DISTINCT 本质上是对所有选定列的 GROUP BY。要在去重的同时保留代表性行(如最新的),使用 ROW_NUMBER() OVER (PARTITION BY ...)。DISTINCT 可能很昂贵——它会构建临时表,所以优先使用索引。

mysql
# unique values of one column
SELECT DISTINCT country FROM users;

# distinct over multiple columns
SELECT DISTINCT city, country FROM users;

# COUNT distinct values
SELECT COUNT(DISTINCT country) AS countries FROM users;

# GROUP BY returns one row per group; DISTINCT is a special case
SELECT country, COUNT(*) AS users
FROM users
GROUP BY country;

# deduplicate while keeping the latest row (8.0+)
WITH latest AS (
  SELECT *, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id DESC) rn
  FROM users
)
SELECT id, email FROM latest WHERE rn = 1;

LIKE 与 REGEXP 模式匹配

LIKE 简单但前导通配符(LIKE '%x')会使索引失效并强制全表扫描。排序规则决定大小写敏感性——utf8mb4_0900_ai_ci 不区分重音/大小写。REGEXP/RLIKE 增加了完整正则能力但从不使用索引。MySQL 8.0 新增 REGEXP_LIKE、REGEXP_REPLACE、REGEXP_INSTR 和 REGEXP_SUBSTR 提供更丰富的模式处理。全文搜索请用 FULLTEXT 索引配合 MATCH ... AGAINST。

mysql
# LIKE wildcards: % (any chars) and _ (one char)
SELECT * FROM users WHERE username LIKE 'a%';     -- starts with a
SELECT * FROM users WHERE email LIKE '%@gmail.com';
SELECT * FROM users WHERE username LIKE '_an';    -- 3 chars, ends 'an'

# escape a wildcard literally
SELECT * FROM users WHERE path LIKE '50\%' ESCAPE '\\';

# case-insensitive LIKE depends on column collation
# *_ci collations are case-insensitive

# REGEXP / RLIKE for regular expressions
SELECT * FROM users WHERE email REGEXP '^[a-z]+@';
SELECT * FROM products WHERE name RLIKE 'iPhone|Galaxy';

# capture groups with REGEXP_REPLACE (8.0+)
SELECT REGEXP_REPLACE(phone, '([0-9]{3})([0-9]{4})', '$1-$2')
FROM contacts;

CASE 表达式

CASE 是 SQL 条件表达式。searched 形式从上到下评估 WHEN 条件,返回第一个匹配的 THEN 值,否则返回 ELSE(或 NULL)。CASE 可用于 SELECT、WHERE、ORDER BY 和聚合中——SUM(CASE ...) 模式是经典的行列转换。CASE 不会短路聚合,所以每一行都会被计数。IF() 和 IFNULL() 是简单场景下更简短的 MySQL 快捷方式。

mysql
# searched CASE (like if/else)
SELECT username,
  CASE
    WHEN age < 18 THEN 'minor'
    WHEN age < 65 THEN 'adult'
    ELSE 'senior'
  END AS age_group
FROM users;

# simple CASE (compare to a value)
SELECT order_id,
  CASE status
    WHEN 0 THEN 'pending'
    WHEN 1 THEN 'paid'
    WHEN 2 THEN 'shipped'
    ELSE 'unknown'
  END AS status_label
FROM orders;

# CASE in aggregate (pivot)
SELECT
  SUM(CASE WHEN status='paid' THEN 1 ELSE 0 END) AS paid_count,
  SUM(CASE WHEN status='shipped' THEN 1 ELSE 0 END) AS shipped_count
FROM orders;
06

JOIN 连接

INNER JOIN

INNER JOIN 仅在两表都有匹配时返回行——任一侧不匹配的行会被丢弃。JOIN 是 INNER JOIN 的简写。USING (col) 在列名相同时是 ON a.col = b.col 的简洁替代,并在结果中只出现一次。INNER JOIN 是关联规范化表最常用的默认连接类型。

mysql
# only matching rows from both tables
SELECT u.username, o.id AS order_id, o.total
FROM users u
INNER JOIN orders o ON o.user_id = u.id
WHERE o.paid = 1;

# join three tables
SELECT u.username, o.id, oi.product_id, oi.qty
FROM users u
JOIN orders o        ON o.user_id   = u.id
JOIN order_items oi  ON oi.order_id = o.id;

# USING clause when join columns share a name
SELECT u.username, o.id
FROM users u
JOIN orders o USING (user_id);

LEFT 与 RIGHT JOIN

LEFT JOIN 保留左表所有行,无匹配时右侧填充 NULL。WHERE o.id IS NULL 技巧(反连接)查找左表中无匹配右行的行——通常比 NOT IN 更清晰更快,尤其有 NULL 时。RIGHT JOIN 是对称的;大多数人会重排表顺序改用 LEFT JOIN 以提高可读性。连接键中的 NULL 永远不匹配。

mysql
# LEFT JOIN: all left rows, with NULLs where no match
SELECT u.username, o.id AS order_id
FROM users u
LEFT JOIN orders o ON o.user_id = u.id;

# find users with NO orders (anti-join pattern)
SELECT u.username
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.id IS NULL;

# RIGHT JOIN is the mirror (rarely used; swap tables instead)
SELECT u.username, o.id
FROM orders o
RIGHT JOIN users u ON o.user_id = u.id;

CROSS JOIN

CROSS JOIN 返回笛卡尔积——两表行的所有组合。适用于生成组合或填充维度矩阵,但产生 N x M 行,大表上代价高昂。表间逗号(FROM a, b)是隐式 CROSS JOIN。递归 CTE 模式是 MySQL 8.0+ 中生成序列/维度表的标准方法。

mysql
# Cartesian product: every row of A paired with every row of B
SELECT s.size, c.color
FROM sizes s
CROSS JOIN colors c;

# same effect with a comma join
SELECT s.size, c.color FROM sizes s, colors c;

# generate a series of dates (no built-in GENERATE_SERIES)
WITH RECURSIVE nums(n) AS (
  SELECT 1 UNION ALL SELECT n+1 FROM nums WHERE n < 7
)
SELECT DATE_ADD('2024-01-01', INTERVAL n-1 DAY) AS day
FROM nums;

# build a matrix of all category x region combinations
SELECT cat.name, reg.name
FROM categories cat CROSS JOIN regions reg;

自连接

自连接用不同别名引用同一表两次,以关联表内行——经典用于经理/员工或邻接表层次结构。a.id < b.id 条件避免重复的对称配对。对于深层层次结构使用递归 CTE(MySQL 8.0+),它无需固定深度限制即可遍历树,并能用 REPEAT/CONCAT 构建缩进树形输出。

mysql
# employees and their managers (same table twice)
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;

# find pairs of users in the same city
SELECT a.username AS user_a, b.username AS user_b, a.city
FROM users a
JOIN users b ON a.city = b.city AND a.id < b.id;

# hierarchical tree with recursive CTE (8.0+)
WITH RECURSIVE org AS (
  SELECT id, name, manager_id, 1 AS depth
  FROM employees WHERE manager_id IS NULL
  UNION ALL
  SELECT e.id, e.name, e.manager_id, o.depth + 1
  FROM employees e JOIN org o ON e.manager_id = o.id
)
SELECT CONCAT(REPEAT('-- ', depth-1), name) AS tree
FROM org;

FULL OUTER JOIN 替代方案

MySQL 不支持 FULL OUTER JOIN。标准模拟方法是 LEFT JOIN UNION RIGHT JOIN——UNION 去重使每个匹配行只出现一次,两侧不匹配的行带有对方的 NULL。大结果集建议使用 UNION ALL 配合去重策略,因为 UNION 的隐式 DISTINCT 代价高。如果只需要不匹配的行,改用两个反连接。

mysql
# MySQL has no FULL OUTER JOIN — emulate with LEFT + UNION + RIGHT
SELECT u.username, o.id AS order_id
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
UNION
SELECT u.username, o.id AS order_id
FROM users u
RIGHT JOIN orders o ON o.user_id = u.id;

# the UNION removes duplicates; use UNION ALL to keep them
# for an anti-union (mismatches only) add WHERE o.id IS NULL
#   or u.id IS NULL in each branch

NATURAL JOIN 与 USING

NATURAL JOIN 很脆弱——它自动连接所有同名列,新增列可能悄悄改变结果;生产环境应避免。USING (col) 更安全,并在输出中合并连接列为一份。STRAIGHT_JOIN 强制优化器按从左到右的顺序连接,是对糟糕执行计划的手动覆盖;修复根本问题后应移除。为清晰起见,优先使用显式 ON。

mysql
# NATURAL JOIN matches all same-named columns automatically
SELECT * FROM users NATURAL JOIN profiles;

# USING matches specific shared columns, dedups them in output
SELECT u.username, user_id
FROM users u
JOIN orders o USING (user_id);

# STRAIGHT_JOIN forces the optimizer to join left-to-right
SELECT STRAIGHT_JOIN u.username, o.id
FROM users u
JOIN orders o ON o.user_id = u.id;
07

聚合与分组

聚合函数

聚合函数将多行折叠为一行:COUNT、SUM、AVG、MIN、MAX。COUNT(*) 计算行数;COUNT(col) 计算 col 的非空值;COUNT(DISTINCT col) 计算唯一值。AVG 忽略 NULL(而非视为零),可能扭曲结果——若要将 NULL 视为零请用 COALESCE(col,0)。无 GROUP BY 的聚合即使在空表上也返回单行。

mysql
SELECT
  COUNT(*)               AS row_count,
  COUNT(email)           AS emails,        -- ignores NULLs
  COUNT(DISTINCT country) AS countries,
  MIN(created_at)        AS first_user,
  MAX(created_at)        AS last_user,
  AVG(age)               AS avg_age,
  SUM(balance)           AS total_balance
FROM users;

# aggregate over a filtered set
SELECT SUM(amount) AS paid_total
FROM orders
WHERE status = 'paid';

GROUP BY

GROUP BY 将共享分组列值的行折叠为一个输出行,每组计算聚合。MySQL 8.0 默认启用 ONLY_FULL_GROUP_BY,拒绝查询 SELECT 中不在 GROUP BY 的列(早期版本返回任意行——常见 bug)。可按表达式或别名分组。对于时间序列报表,按时间戳的 YEAR()/MONTH()/DATE() 分组。

mysql
# one row per country
SELECT country, COUNT(*) AS users, AVG(age) AS avg_age
FROM users
GROUP BY country;

# group by multiple columns
SELECT country, city, COUNT(*) AS users
FROM users
GROUP BY country, city
ORDER BY country, users DESC;

# group by expression
SELECT YEAR(created_at) AS yr, MONTH(created_at) AS m, COUNT(*)
FROM users
GROUP BY yr, m;

# MySQL 8.0 enforces ONLY_FULL_GROUP_BY:
# every non-aggregated SELECT column must be in GROUP BY

HAVING

HAVING 过滤聚合后的结果,WHERE 在分组前过滤输入行——所以 HAVING 可引用聚合(SUM、COUNT),WHERE 不能。将行级过滤放在 WHERE 中以提高效率(减少分组前的行数)。MySQL 中 HAVING 接受别名(HAVING users > 100)。查询可同时使用两者:WHERE 缩小行,GROUP BY 分桶,HAVING 缩小桶。

mysql
# HAVING filters groups (after aggregation); WHERE filters rows (before)
SELECT country, COUNT(*) AS users
FROM users
GROUP BY country
HAVING COUNT(*) > 100
ORDER BY users DESC;

# filter on an aggregate
SELECT user_id, SUM(amount) AS total
FROM orders
GROUP BY user_id
HAVING SUM(amount) > 1000;

# combine WHERE and HAVING
SELECT country, COUNT(*) AS users, AVG(age) AS avg_age
FROM users
WHERE status = 'active'
GROUP BY country
HAVING avg_age >= 30;

GROUP BY ROLLUP

WITH ROLLUP 为每个分组级别添加超聚合行和总计行,分组列变为 NULL。GROUPING(col) 在小计/总计行返回 1,可将 NULL 标记为 'ALL'。ROLLUP 按 GROUP BY 列从左到右工作。MySQL 没有 WITH CUBE——如需每个维度组合,用多个 ROLLUP 查询的 UNION ALL 模拟。

mysql
# ROLLUP adds subtotals and a grand total row
SELECT country, city, COUNT(*) AS users
FROM users
GROUP BY country, city WITH ROLLUP;

# output includes:
#   country | city    | users
#   'US'    | 'NYC'   | 120
#   'US'    | NULL    | 200   <- US subtotal
#   NULL    | NULL    | 950   <- grand total

# identify super-aggregate rows with GROUPING()
SELECT
  IF(GROUPING(country)=1,'ALL',country) AS country,
  IF(GROUPING(city)=1,'ALL',city)       AS city,
  COUNT(*) AS users
FROM users
GROUP BY country, city WITH ROLLUP;

GROUP_CONCAT

GROUP_CONCAT 是 MySQL 的字符串聚合函数——将一组的值连接为单个字符串。用 DISTINCT 去重,ORDER BY 排序,SEPARATOR 更改分隔符(默认 ',')。结果上限为 group_concat_max_len(默认 1024 字节);较长列表需调高。其他数据库称之为 LISTAGG/STRING_AGG——GROUP_CONCAT 是 MySQL 的等价物。

mysql
# concatenate grouped values into one string
SELECT country,
  GROUP_CONCAT(username) AS all_names
FROM users
GROUP BY country;

# order within the list and deduplicate
SELECT user_id,
  GROUP_CONCAT(DISTINCT tag ORDER BY tag SEPARATOR ',') AS tags
FROM post_tags
GROUP BY user_id;

# the default separator is ',', max length is 1024 by default
# raise the limit:
# SET SESSION group_concat_max_len = 1000000;

SELECT department,
  GROUP_CONCAT(name ORDER BY salary DESC SEPARATOR ' | ') AS ranking
FROM employees
GROUP BY department;

ROLLUP 与窗口聚合对比

ROLLUP 通过添加小计行来缩减结果,而窗口聚合(OVER)保持相同行数并将聚合作为额外列附加——因此可以同时看到明细和总计。汇总报表用 ROLLUP,需要逐行上下文加聚合时用窗口函数。累计总和是 SUM OVER (ORDER BY ...) 的经典用例。

mysql
# ROLLUP gives fewer rows (subtotals mixed into the result)
SELECT country, city, COUNT(*) AS users
FROM users
GROUP BY country, city WITH ROLLUP;

# a window aggregate keeps every detail row AND adds the total
SELECT
  country, city,
  COUNT(*)              AS users_in_city,
  COUNT(*) OVER (PARTITION BY country) AS users_in_country,
  COUNT(*) OVER ()                      AS users_total
FROM users;

# running total over time
SELECT created_at, amount,
  SUM(amount) OVER (ORDER BY created_at) AS running_total
FROM daily_sales;
08

子查询与 CTE

标量子查询

标量子查询返回单个值(一行一列),可在任何表达式有效的地方使用——SELECT 列表、WHERE、HAVING。若无返回行则值为 NULL。优化器通常能将相关标量子查询转换为连接(子查询物化)。为清晰起见以及有时性能更好,当同一子查询被多次引用时,CTE 或 JOIN 更合适。

mysql
# returns a single value
SELECT username, balance,
  (SELECT AVG(balance) FROM users) AS avg_balance
FROM users
WHERE balance > (SELECT AVG(balance) FROM users);

# use in SELECT list, WHERE, HAVING, etc.
SELECT product, price,
  price - (SELECT MIN(price) FROM products) AS above_min
FROM products;

# a scalar subquery must return exactly one row, one column

IN / ANY / ALL 子查询

IN 匹配子查询返回的任意值;NOT IN 是其补充但被 NULL 毒化——如果子查询返回 NULL,NOT IN 完全不返回行,所以始终排除 NULL 或使用 NOT EXISTS。ALL 在比较对所有返回值成立时为真;ANY/SOME 在至少一个成立时为真。优化器通常将这些重写为半连接。

mysql
# IN: value matches any row of the subquery
SELECT * FROM orders
WHERE user_id IN (SELECT id FROM users WHERE status='active');

# NOT IN with NULLs is dangerous — prefer NOT EXISTS
SELECT * FROM users
WHERE id NOT IN (SELECT user_id FROM orders WHERE user_id IS NOT NULL);

# ANY / SOME / ALL with comparisons
SELECT * FROM products
WHERE price > ALL (SELECT price FROM products WHERE category='toys');

SELECT * FROM products
WHERE price > ANY (SELECT price FROM products WHERE category='toys');

EXISTS 与 NOT EXISTS

EXISTS 测试是否存在行而不返回数据——它在第一个匹配处停止扫描,因此用于存在性检查很高效。子查询是相关的(引用外层查询)。NOT EXISTS 是查找无匹配行的 NULL 安全方式,通常优于 NOT IN。EXISTS 内的 SELECT 1 是惯例;列列表无关紧要。优化器通常将 EXISTS 转为半连接。

mysql
# EXISTS: true if the subquery returns any rows
SELECT u.username
FROM users u
WHERE EXISTS (
  SELECT 1 FROM orders o WHERE o.user_id = u.id
);

# NOT EXISTS: the standard anti-semi-join (NULL-safe)
SELECT u.username
FROM users u
WHERE NOT EXISTS (
  SELECT 1 FROM orders o WHERE o.user_id = u.id
);

# correlated: the subquery references the outer row (u.id)
# EXISTS stops at the first matching row — efficient for presence checks

派生表

派生表是 FROM 子句中的子查询——它物化一个可从中查询或连接的中间结果。必须为其取别名。MySQL 8.0 可将许多派生表合并到外层查询(derived_merge)以获得更好计划,但复杂聚合或 LIMIT 通常强制物化。为可读性和查询内复用,优先使用命名 CTE(WITH)而非内联派生表。

mysql
# a subquery in the FROM clause is a derived table
SELECT t.country, t.users
FROM (
  SELECT country, COUNT(*) AS users
  FROM users
  GROUP BY country
) t
WHERE t.users > 50;

# join a derived table
SELECT u.username, agg.order_count
FROM users u
JOIN (
  SELECT user_id, COUNT(*) AS order_count
  FROM orders
  GROUP BY user_id
) agg ON agg.user_id = u.id;

# derived tables must be aliased

相关子查询

相关子查询引用外层查询的列,因此逻辑上为每个外层行重新求值——大集合上可能很慢。优化器可能将其转换为连接或使用缓存(子查询缓存)来缓解。对于排名/Top-N 问题(如第 N 高工资),窗口函数如 DENSE_RANK() OVER (ORDER BY salary DESC) 更清晰更快——MySQL 8.0+ 上应优先使用。

mysql
# the subquery references the outer row (u.id) and runs per row
SELECT u.username,
  (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS order_count
FROM users u;

# correlated subquery in WHERE (nth-highest salary classic)
SELECT e1.name, e1.salary
FROM employees e1
WHERE 2 = (
  SELECT COUNT(DISTINCT e2.salary)
  FROM employees e2
  WHERE e2.salary > e1.salary
);

# prefer a window function for rank-style problems in 8.0+

公共表表达式 (CTE)

CTE(WITH 子句)为子查询命名,提高可读性和在一条语句内复用;MySQL 8.0+ 支持。RECURSIVE 启用自引用 CTE——对树遍历和序列生成至关重要(UNION ALL 递归,UNION 去重)。递归 CTE 需要基础用例、递归分支和终止条件。多个 CTE 用逗号分隔,可在同一 WITH 中引用前面的 CTE。

mysql
# a named CTE (WITH) — readable, can be referenced multiple times
WITH active_users AS (
  SELECT id, username FROM users WHERE status = 'active'
)
SELECT au.username, COUNT(o.id) AS orders
FROM active_users au
LEFT JOIN orders o ON o.user_id = au.id
GROUP BY au.username;

# RECURSIVE CTE: walk a hierarchy or generate a series
WITH RECURSIVE nums(n) AS (
  SELECT 1
  UNION ALL
  SELECT n + 1 FROM nums WHERE n < 10
)
SELECT n FROM nums;

# multiple CTEs, comma-separated
WITH a AS (SELECT ...), b AS (SELECT ...)
SELECT * FROM a JOIN b USING (id);
09

索引

创建索引

索引是一种数据结构(InnoDB 中的 B+Tree),以写入开销和存储为代价加速查找和排序。在 WHERE、JOIN 和 ORDER BY 使用的列上创建索引。UNIQUE 索引还强制唯一性约束。长文本列的前缀索引节省空间但不能用于覆盖扫描或 ORDER BY。InnoDB 中每个主键都是聚簇的——二级索引存储 PK 值作为行指针。

mysql
# single-column index
CREATE INDEX idx_email ON users(email);

# unique index (enforces uniqueness, allows fast lookup)
CREATE UNIQUE INDEX uq_username ON users(username);

# create with a table
CREATE TABLE users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  email VARCHAR(255),
  UNIQUE KEY uq_email (email)
);

# full-table prefix index (limited length)
CREATE INDEX idx_name ON users(last_name(20));

复合索引

复合索引按最左列优先排序,所以只对查询过滤列列表最左前缀有帮助——列顺序至关重要。将最具选择性的等值列放前面,范围列放后面。覆盖索引包含查询所需的所有列,允许仅索引扫描完全跳过表查找(EXPLAIN 显示 'Using index')。不要过度索引——每个索引都会拖慢写入。

mysql
# multi-column index (column order matters!)
CREATE INDEX idx_last_first ON users(last_name, first_name);

# supports these lookups (leftmost prefix rule):
#   WHERE last_name = 'X'                       -> uses index
#   WHERE last_name = 'X' AND first_name = 'Y'  -> uses index
#   WHERE first_name = 'Y'                      -> CANNOT use index

# covering index: all needed columns are in the index
CREATE INDEX idx_cover ON orders(user_id, status, amount);
SELECT user_id, SUM(amount) FROM orders
WHERE user_id = 5 AND status = 'paid' GROUP BY user_id;
# ^ index-only scan, no table lookup needed

全文索引

FULLTEXT 索引支持自然语言和布尔文本搜索,远优于 LIKE '%word%'。MATCH ... AGAINST 返回相关度得分。布尔模式支持运算符:+要求、-排除、*通配、>提升、~降低。需要 InnoDB(5.6+)或 MyISAM 以及最小词长(ft_min_word_len / innodb_ft_min_token_size,默认 3)。大规模严肃搜索考虑专用引擎如 Elasticsearch。

mysql
CREATE TABLE articles (
  id INT AUTO_INCREMENT PRIMARY KEY,
  title VARCHAR(200),
  body TEXT,
  FULLTEXT KEY ft_title_body (title, body)
) ENGINE=InnoDB;

# natural-language search
SELECT id, title
FROM articles
WHERE MATCH(title, body) AGAINST ('database performance');

# boolean mode: +require -exclude ~relax
SELECT id, title
FROM articles
WHERE MATCH(title, body)
  AGAINST ('+MySQL -Oracle >index' IN BOOLEAN MODE);

# query expansion (second pass adds related terms)
SELECT id FROM articles
WHERE MATCH(title, body) AGAINST ('database' WITH QUERY EXPANSION);

空间索引

空间(R-tree)索引加速 GEOMETRY/POINT/POLYGON 列上的地理查询。MySQL 8.0 标准化使用 SRID 4326(WGS 84 GPS 坐标)并新增 ST_Distance_Sphere 计算真实世界米距离。列必须为 NOT NULL。用 ST_Within/ST_Contains/ST_Distance 过滤。始终以(经度, 纬度)表示坐标以匹配 X/Y 约定。空间索引使半径查询大幅加速。

mysql
CREATE TABLE places (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(100),
  location POINT NOT NULL SRID 4326,
  SPATIAL KEY sp_location (location)
) ENGINE=InnoDB;

# insert a point (longitude, latitude)
INSERT INTO places (name, location)
VALUES ('HQ', ST_PointFromText('POINT(116.40 39.90)', 4326));

# find points within a bounding distance
SELECT name, ST_Distance_Sphere(location,
  ST_PointFromText('POINT(116.40 39.90)', 4326)) AS meters
FROM places
WHERE ST_Within(location,
  ST_Buffer(ST_PointFromText('POINT(116.40 39.90)', 4326), 0.01));

索引管理

SHOW INDEX 列出表的索引及列顺序和基数(不同值估计)。DROP INDEX 删除索引;重命名(8.0+)避免删除+重建。OPTIMIZE TABLE 重建表以回收碎片空间并刷新索引统计——锁较重,应在低流量时执行。过时统计可能误导优化器;大批数据加载后运行 ANALYZE TABLE 刷新。

mysql
# show indexes on a table
SHOW INDEX FROM users;

# drop an index
DROP INDEX idx_email ON users;
ALTER TABLE users DROP INDEX uq_username;

# rename an index (8.0+)
ALTER TABLE users RENAME INDEX idx_email TO idx_user_email;

# rebuild all indexes on a table
ALTER TABLE users ENGINE=InnoDB;
OPTIMIZE TABLE users;

# check index cardinality
SHOW INDEX FROM users;
SELECT index_name, cardinality
FROM information_schema.statistics
WHERE table_schema='mydb' AND table_name='users';

不可见索引与提示

不可见索引(8.0+)可安全测试移除索引——优化器忽略它但写入仍维护,如果查询退化可立即恢复;确认后删除即可。优化器提示(/*+ ... */)优于旧的 USE/FORCE/IGNORE INDEX 语法,且只影响标注的语句。谨慎使用提示——它们是糟糕数据分布或缺失统计的权宜之计。

mysql
# make an index invisible (8.0+): optimizer ignores it, still maintained
ALTER TABLE users ALTER INDEX idx_email SET INVISIBLE;
# ... observe query plans, then drop or restore:
ALTER TABLE users ALTER INDEX idx_email SET VISIBLE;

# optimizer hints (8.0+) to force / avoid indexes
SELECT /*+ INDEX(u idx_email) */ * FROM users u WHERE email LIKE 'a%';
SELECT /*+ NO_INDEX(u idx_email) */ * FROM users u;
SELECT /*+ FORCE INDEX(u idx_email) */ * FROM users u;

# index merge / IGNORE_INDEX legacy hint
SELECT * FROM users USE INDEX (idx_email) WHERE email = '[email protected]';
SELECT * FROM users IGNORE INDEX (idx_email) WHERE email = '[email protected]';
10

视图

创建视图

视图是存储的查询,行为类似虚拟表——它简化复杂查询、抽象 schema 变更并强制行/列可见性。CREATE OR REPLACE 一步更新定义。视图不存储数据(除非物化,每次都重新运行底层查询),因此无写入开销但也没有读取加速。视图在可能时会被合并到查询中。

mysql
# a view is a stored SELECT
CREATE VIEW active_users AS
SELECT id, username, email
FROM users
WHERE status = 'active';

# query it like a table
SELECT * FROM active_users WHERE username LIKE 'a%';

# create or replace, with explicit columns
CREATE OR REPLACE VIEW user_summary (username, order_count, total) AS
SELECT u.username, COUNT(o.id), COALESCE(SUM(o.amount), 0)
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.username;

# list and inspect views
SHOW FULL TABLES WHERE Table_type = 'VIEW';
SHOW CREATE VIEW active_users\G

可更新视图

基于单个基础表且无聚合/去重的视图是可更新的——INSERT/UPDATE/DELETE 传播到底层表。默认情况下,将行移出视图 WHERE 条件的 UPDATE 会成功(行从视图中消失);WITH CHECK OPTION 阻止此类更改,使行永远不会离开视图范围。带 JOIN、GROUP BY、DISTINCT 或子查询的视图通常不可更新。

mysql
# a view over a single base table is updatable
CREATE VIEW active_users AS
SELECT id, username, email, status
FROM users
WHERE status = 'active';

# INSERT through the view inserts into the base table
INSERT INTO active_users (id, username, email, status)
VALUES (10, 'newbie', '[email protected]', 'active');

# UPDATE through the view only touches visible rows
UPDATE active_users SET email = '[email protected]' WHERE id = 10;

# WITH CHECK OPTION prevents inserts/updates that leave the view
CREATE OR REPLACE VIEW active_users AS
SELECT id, username, email, status FROM users WHERE status='active'
WITH CHECK OPTION;

视图 CHECK OPTION

CHECK OPTION 强制通过视图更改的行保持对该视图可见。LOCAL 仅检查视图自身的 WHERE;CASCADED(默认)还强制其引用的底层视图的 WHERE 子句。叠加视图时用 CASCADED 保持整个链一致;仅当刻意要放松底层视图过滤时才用 LOCAL。

mysql
# LOCAL: only the defining view's WHERE is enforced
CREATE VIEW v_paid AS
  SELECT * FROM orders WHERE status='paid' WITH LOCAL CHECK OPTION;

# CASCADED (default): all underlying views' checks are enforced too
CREATE VIEW v_paid_large AS
  SELECT * FROM v_paid WHERE amount > 100
  WITH CASCADED CHECK OPTION;

# an insert that violates v_paid's filter:
INSERT INTO v_paid_large (id, status, amount) VALUES (1, 'unpaid', 200);
# CASCADED -> rejected (status would leave v_paid)
# LOCAL    -> accepted (only v_paid_large's filter checked)

管理视图

ALTER VIEW 在不删除的情况下重定义视图(保留授权)。DROP VIEW IF EXISTS 在视图不存在时避免报错。RENAME TABLE 也适用于视图但不常见。information_schema.views 暴露每个视图的定义、是否可更新以及安全类型——对审计很有用。视图依赖其基础表;删除基础表会使视图失效直到重建。

mysql
# alter a view's definition (CREATE OR REPLACE is simpler)
ALTER VIEW active_users AS
  SELECT id, username FROM users WHERE status='active';

# drop a view
DROP VIEW IF EXISTS active_users;

# drop multiple views
DROP VIEW IF EXISTS v1, v2, v3;

# rename a view (no RENAME VIEW; recreate or use a table rename trick)
RENAME TABLE old_view TO new_view;

# view metadata
SELECT table_name, view_definition, is_updatable
FROM information_schema.views
WHERE table_schema = 'mydb';

视图安全性与确定性

SQL SECURITY DEFINER 以定义者权限运行视图——可用于向无基础表访问权限的用户暴露过滤后的数据(如行级安全视图)。INVOKER 改为检查调用者权限。ALGORITHM MERGE 将视图折叠到查询中(更快,允许可更新视图);TEMPTABLE 先物化(某些结构需要但失去可更新性且可能更慢)。UNDEFINED 让 MySQL 选择。

mysql
# DEFINER (default): runs with the view creator's privileges
CREATE VIEW secret_emails SQL SECURITY DEFINER AS
  SELECT email FROM users;

# INVOKER: runs with the querying user's privileges
CREATE VIEW my_emails SQL SECURITY INVOKER AS
  SELECT email FROM users WHERE id = CURRENT_USER_ID();

# ALGORITHM: MERGE (preferred) vs TEMPTABLE vs UNDEFINED
CREATE ALGORITHM=MERGE VIEW v_active AS
  SELECT * FROM users WHERE status='active';
11

存储过程

创建与调用过程

存储过程是命名的、预编译的 SQL 块。因为过程可包含分号,需更改 DELIMITER 使整个 CREATE 作为一条语句执行,然后重置。CALL 执行过程。过程不直接返回值但可返回结果集和修改 OUT 参数。它们在服务器端运行,减少多步骤逻辑的网络往返,但比应用代码更难版本化和测试。

mysql
DELIMITER //
CREATE PROCEDURE get_user_by_id(IN p_id INT)
BEGIN
  SELECT id, username, email FROM users WHERE id = p_id;
END //
DELIMITER ;

# call it
CALL get_user_by_id(42);

# drop it
DROP PROCEDURE IF EXISTS get_user_by_id;

# show the definition
SHOW CREATE PROCEDURE get_user_by_id\G

IN / OUT / INOUT 参数

参数分为 IN(只读输入)、OUT(通过会话变量写回调用者)或 INOUT(两者皆是)。SELECT ... INTO 将标量查询结果赋给变量。示例将借/贷包装在事务中并用 SELECT FOR UPDATE 锁定行以防止丢失更新。用 OUT 参数返回状态码;在过程中 SELECT 时直接返回结果集。

mysql
DELIMITER //
CREATE PROCEDURE transfer(
  IN  p_from   INT,
  IN  p_to     INT,
  IN  p_amount DECIMAL(10,2),
  OUT p_result VARCHAR(50)
)
BEGIN
  DECLARE v_bal DECIMAL(10,2);
  START TRANSACTION;
  SELECT balance INTO v_bal FROM accounts WHERE id = p_from FOR UPDATE;
  IF v_bal >= p_amount THEN
    UPDATE accounts SET balance = balance - p_amount WHERE id = p_from;
    UPDATE accounts SET balance = balance + p_amount WHERE id = p_to;
    COMMIT;
    SET p_result = 'OK';
  ELSE
    ROLLBACK;
    SET p_result = 'INSUFFICIENT_FUNDS';
  END IF;
END //
DELIMITER ;

# call with a session variable for the OUT value
CALL transfer(1, 2, 50.00, @res);
SELECT @res;

变量与控制流

存储程序支持局部变量(DECLARE,作用域为 BEGIN...END)、用户变量(@var,会话级)和系统变量。控制流包括 IF/ELSEIF/ELSE、CASE 和三种循环:LOOP(配合 LEAVE/ITERATE)、WHILE(条件在前)和 REPEAT(条件在后)。嵌套时始终命名循环以便 LEAVE 定向到正确的循环。SET 赋值;SELECT ... INTO 将查询标量复制到变量。

mysql
DELIMITER //
CREATE PROCEDURE classify(IN p_age INT, OUT p_label VARCHAR(20))
BEGIN
  DECLARE v_count INT DEFAULT 0;

  # IF / ELSEIF / ELSE
  IF p_age < 13 THEN
    SET p_label = 'child';
  ELSEIF p_age < 20 THEN
    SET p_label = 'teen';
  ELSE
    SET p_label = 'adult';
  END IF;

  # simple loop with a counter
  simple_loop: LOOP
    SET v_count = v_count + 1;
    IF v_count >= 5 THEN LEAVE simple_loop; END IF;
  END LOOP;

  # WHILE / REPEAT alternatives
  # WHILE cond DO ... END WHILE;
  # REPEAT ... UNTIL cond END REPEAT;

  # CASE statement
  CASE p_label
    WHEN 'child' THEN SET p_label = CONCAT(p_label, '!');
    ELSE SET p_label = CONCAT(p_label, '.');
  END CASE;
END //
DELIMITER ;

游标

游标让存储过程逐行遍历结果集。先声明游标,再声明 CONTINUE HANDLER FOR NOT FOUND 检测耗尽(设置标志并 LEAVE 循环)。FETCH 将一行读入变量。MySQL 中游标是只读且只能向前的。逐行处理比集合式 SQL 慢——可能时优先使用单条 UPDATE/INSERT ... SELECT;仅在逻辑确实需要顺序处理时使用游标。

mysql
DELIMITER //
CREATE PROCEDURE log_all_users()
BEGIN
  DECLARE v_id INT;
  DECLARE v_name VARCHAR(50);
  DECLARE done INT DEFAULT 0;

  # declare the cursor BEFORE handlers
  DECLARE cur CURSOR FOR
    SELECT id, username FROM users WHERE status='active';

  # continue handler for end of cursor -> set done=1
  DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;

  OPEN cur;
  read_loop: LOOP
    FETCH cur INTO v_id, v_name;
    IF done THEN LEAVE read_loop; END IF;
    INSERT INTO audit_log(user_id, action) VALUES (v_id, CONCAT('seen:', v_name));
  END LOOP;
  CLOSE cur;
END //
DELIMITER ;

CALL log_all_users();

错误处理程序

处理程序捕获存储程序内的错误:CONTINUE 执行处理程序体后继续执行失败语句之后的内容;EXIT 执行体并退出 BEGIN...END 块;UNDO 回滚(已弃用)。用 SQLEXCEPTION 捕获任何错误,SQLWARNING 捕获警告,或特定 SQLSTATE/errno(如 1062 重复键)。RESIGNAL 在清理后重新引发当前错误。命名条件(CONDITION FOR)使处理程序更易读。

mysql
DELIMITER //
CREATE PROCEDURE safe_insert(IN p_email VARCHAR(255))
BEGIN
  # SQLEXCEPTION catches any error; SQLSTATE/errno catch specific ones
  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    ROLLBACK;
    RESIGNAL;  -- re-raise so the caller sees the error
  END;

  DECLARE CONTINUE HANDLER FOR 1062  -- duplicate key
  BEGIN
    SELECT 'Duplicate skipped' AS msg;
  END;

  START TRANSACTION;
  INSERT INTO users (username, email) VALUES ('x', p_email);
  INSERT INTO stats (email) VALUES (p_email);
  COMMIT;
END //
DELIMITER ;

# named conditions make handlers readable
DECLARE dup_key CONDITION FOR 1062;
DECLARE CONTINUE HANDLER FOR dup_key SELECT 'dup';

过程元数据

SHOW PROCEDURE STATUS 列出过程;SHOW CREATE PROCEDURE 返回完整过程体。information_schema.routines 提供更丰富的元数据,包括确定性和安全类型。如果函数输出仅取决于输入则标记 DETERMINISTIC(READS SQL DATA / NO SQL)——这是二进制日志和函数复制所必需的。过程不需要确定性标志。用这些视图审计和清点例程。

mysql
# list procedures in a database
SHOW PROCEDURE STATUS WHERE Db = 'mydb';

# show definition
SHOW CREATE PROCEDURE get_user_by_id\G

# query information_schema for routines
SELECT routine_name, routine_type, created, last_altered,
       is_deterministic, security_type
FROM information_schema.routines
WHERE routine_schema = 'mydb';

# list stored functions too
SELECT routine_name, routine_type
FROM information_schema.routines
WHERE routine_schema = 'mydb' AND routine_type='FUNCTION';
12

函数

字符串函数

LENGTH 计算字节数,CHAR_LENGTH 计算字符数(对多字节 utf8mb4 很重要)。SUBSTRING 从 1 开始索引;负起始值从末尾计数。CONCAT 在任一参数为 NULL 时返回 NULL——使用 CONCAT_WS 跳过 NULL。SUBSTRING_INDEX 按分隔符分割并保留前 N 部分(负 N 从右侧保留)。TRIM 有 BOTH/LEADING/TRAILING 变体,可修剪任何字符而非仅空格。

mysql
# length & case
SELECT LENGTH('hello'), CHAR_LENGTH('héllo');  -- bytes vs chars
SELECT UPPER('abc'), LOWER('ABC');

# substring & position
SELECT SUBSTRING('MySQL', 3);           -- 'SQL'
SELECT SUBSTRING('MySQL', 1, 2);        -- 'My'
SELECT INSTR('MySQL', 'SQL');           -- 3
SELECT LOCATE('SQL', 'MySQL', 1);       -- 3

# trim & pad
SELECT TRIM('  hi  '), LTRIM('  hi'), RTRIM('hi  ');
SELECT LPAD('5', 3, '0'), RPAD('5', 3, '-');  -- 005, 5--

# split & replace
SELECT REPLACE('a-b-c', '-', '/');
SELECT SUBSTRING_INDEX('a,b,c', ',', 2);       -- 'a,b'

# concatenate with NULL-safe separator
SELECT CONCAT_WS(',', 'a', NULL, 'b');         -- 'a,b'

数值函数

ROUND 四舍五入远离零;TRUNCATE 截断数字而不四舍五入。MOD 和 % 可互换。RAND() 返回 [0,1) 中的浮点数;ORDER BY RAND() 是随机取行的便捷但昂贵方式,因为它排序整个结果集——大表请改用随机 PK 范围或蓄水池采样。FORMAT 添加千位分隔符并四舍五入到指定位数,返回字符串。

mysql
# rounding
SELECT ROUND(2.567, 1);   -- 2.6
SELECT CEIL(2.1), FLOOR(2.9);   -- 3, 2
SELECT TRUNCATE(2.567, 1);      -- 2.5

# modular & power
SELECT MOD(17, 5), 17 % 5;      -- 2, 2
SELECT POWER(2, 10);            -- 1024
SELECT SQRT(16);                -- 4

# random
SELECT RAND();                  -- 0..1 float
SELECT FLOOR(RAND() * 100);     -- 0..99 integer
SELECT * FROM users ORDER BY RAND() LIMIT 5;  -- random sample (slow!)

# formatting
SELECT FORMAT(1234567.891, 2);  -- '1,234,567.89'
SELECT ABS(-5), SIGN(-5);       -- 5, -1

日期与时间函数

NOW() 一次调用返回当前日期/时间(在一条语句中一致);CURDATE/CURTIME 返回日期/时间部分。DATE_ADD/SUB 配合 INTERVAL 是规范的日期运算——它能正确处理月/年跨年。DATEDIFF 返回天数(date1 - date2);TIMEDIFF 返回 TIME。DATE_FORMAT 和 STR_TO_DATE 使用 %Y %m %d %H %i %s 说明符(注意分钟是小写 i,秒是小写 s)。

mysql
# current values
SELECT NOW(), CURDATE(), CURTIME(), UTC_TIMESTAMP();

# parts of a date
SELECT YEAR(NOW()), MONTH(NOW()), DAY(NOW()), DAYNAME(NOW());
SELECT WEEK(NOW()), WEEKDAY(NOW()), DAYOFWEEK(NOW());

# arithmetic
SELECT DATE_ADD(NOW(), INTERVAL 7 DAY);
SELECT DATE_SUB('2024-01-31', INTERVAL 1 MONTH);  -- 2023-12-31
SELECT DATEDIFF('2024-12-31', '2024-01-01');      -- 365 days
SELECT TIMEDIFF('18:00:00', '09:30:00');          -- 08:30:00

# formatting
SELECT DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s');
SELECT STR_TO_DATE('31/12/2024', '%d/%m/%Y');

自定义函数

存储函数返回单个标量值,可在表达式内使用(不同于需要 CALL 的过程)。它必须声明特性:DETERMINISTIC(相同输入始终产生相同输出,基于语句的复制所需),以及 READS SQL DATA / NO SQL / MODIFIES SQL DATA 之一。函数不能返回结果集或使用事务控制。NO SQL + DETERMINISTIC 组合对纯计算最安全。

mysql
DELIMITER //
CREATE FUNCTION full_name(p_first VARCHAR(50), p_last VARCHAR(50))
RETURNS VARCHAR(101)
DETERMINISTIC
READS SQL DATA
BEGIN
  RETURN CONCAT_WS(' ', p_first, p_last);
END //
DELIMITER ;

# use it like a built-in
SELECT id, full_name(first_name, last_name) FROM users;

# drop it
DROP FUNCTION IF EXISTS full_name;

# functions MUST declare their nature for binary logging:
#   DETERMINISTIC  - same inputs -> same output (required for replication)
#   READS SQL DATA - reads but doesn't modify
#   NO SQL         - no SQL at all

控制流函数

IF() 是紧凑的三元运算符;IFNULL(a,b) 在 a 非 NULL 时返回 a 否则返回 b(仅两参数);COALESCE 推广到 N 个参数。NULLIF(a,b) 在 a=b 时返回 NULL,便于避免除以零(x / NULLIF(y,0))。ISNULL() 返回 1/0 而非值——不要与 IFNULL 混淆。CASE 是最灵活且可移植的条件表达式,适用于任何 SQL 方言。

mysql
# IF(test, true_val, false_val) - ternary
SELECT IF(age >= 18, 'adult', 'minor') FROM users;

# IFNULL / NULLIF
SELECT IFNULL(email, 'no-email') FROM users;          -- coalesce one
SELECT COALESCE(nick, email, username) FROM users;    -- coalesce many
SELECT NULLIF(status, 'inactive');  -- NULL if equal, else status

# CASE as a function (see SELECT section for full CASE)
SELECT CASE WHEN x>0 THEN 'pos' WHEN x<0 THEN 'neg' ELSE 'zero' END;

# ISNULL() returns 1/0 (different from IFNULL!)
SELECT ISNULL(email) FROM users;

类型转换

CAST 和 CONVERT 改变值的类型——当 MySQL 否则会进行使索引失效的隐式转换时(如将字符串与数字列比较)至关重要。CAST(... AS DATE/DATETIME/SIGNED/UNSIGNED/CHAR/DECIMAL) 覆盖常见需求。注意:CAST('123abc' AS SIGNED) 返回 123 并发出警告而非报错。解析自定义日期格式请用 STR_TO_DATE,因为 CAST 要求严格的 ISO 格式。

mysql
# CAST to a specific type
SELECT CAST('2024-01-15' AS DATE);
SELECT CAST(3.14 AS SIGNED);          -- 3
SELECT CAST('123abc' AS SIGNED);      -- 123 (warns in strict mode)
SELECT CAST(123 AS CHAR(10));

# CONVERT is the same thing with different syntax
SELECT CONVERT('2024-01-15', DATE);
SELECT CONVERT('hello' USING utf8mb4);  -- charset conversion

# common: string -> date for comparison
SELECT * FROM orders
WHERE created_at >= CAST('2024-01-01' AS DATETIME);

# format a number as a zero-padded string
SELECT LPAD(CAST(id AS CHAR), 6, '0') FROM orders;
13

触发器

BEFORE / AFTER 触发器

触发器在 INSERT/UPDATE/DELETE 时自动触发,分 BEFORE(可修改 NEW 值、验证并中止)或 AFTER(用于日志等副作用)。FOR EACH ROW 表示体对每个受影响行执行一次——行级触发器是 MySQL 支持的唯一类型。BEFORE 触发器适合默认值/规范化数据;AFTER 触发器用于级联非关键工作。触发器与触发语句在同一事务中运行。

mysql
DELIMITER //
CREATE TRIGGER trg_users_before_insert
BEFORE INSERT ON users
FOR EACH ROW
BEGIN
  # normalize email to lowercase before saving
  SET NEW.email = LOWER(NEW.email);
  IF NEW.created_at IS NULL THEN
    SET NEW.created_at = NOW();
  END IF;
END //
DELIMITER ;

# AFTER INSERT trigger logs the event
CREATE TRIGGER trg_users_after_insert
AFTER INSERT ON users
FOR EACH ROW
INSERT INTO audit_log(table_name, row_id, action, at)
VALUES ('users', NEW.id, 'INSERT', NOW());

审计触发器 (UPDATE)

触发器的常见用途是审计变更——将 OLD 与 NEW 捕获到历史表。比较值必须考虑 NULL(若任一侧为 NULL,OLD.email <> NEW.email 结果为 NULL),所以需用 IS NULL 检查保护。AFTER DELETE 使用 OLD(删除时无 NEW)。保持审计逻辑轻量,因为它在每次行变更时运行。记住批量 UPDATE/DELETE 也会触发触发器,大操作上可能很慢。

mysql
DELIMITER //
CREATE TRIGGER trg_users_after_update
AFTER UPDATE ON users
FOR EACH ROW
BEGIN
  IF OLD.email <> NEW.email OR (OLD.email IS NULL) <> (NEW.email IS NULL) THEN
    INSERT INTO audit_log(table_name, row_id, field, old_val, new_val, at)
    VALUES ('users', NEW.id, 'email', OLD.email, NEW.email, NOW());
  END IF;
END //
DELIMITER ;

# a DELETE audit trigger
CREATE TRIGGER trg_users_after_delete
AFTER DELETE ON users
FOR EACH ROW
INSERT INTO archive_users(id, username, email, deleted_at)
VALUES (OLD.id, OLD.username, OLD.email, NOW());

OLD 和 NEW 值

NEW 持有传入行值,OLD 持有之前的值。可在 BEFORE INSERT/UPDATE 中修改 NEW 以规范化或默认数据;AFTER 触发器和 DELETE 中为只读。SIGNAL SQLSTATE '45000' 是从触发器或存储程序引发自定义错误的标准方式(45000 表示'未处理的用户定义异常')。BEFORE 触发器用于应中止变更的验证;AFTER 用于副作用。

mysql
# OLD: the row before the change (available on UPDATE/DELETE)
# NEW: the row after the change  (available on INSERT/UPDATE)

# BEFORE INSERT: only NEW, you can modify it
# AFTER  INSERT: only NEW, read-only
# BEFORE UPDATE: both OLD and NEW, you can modify NEW
# AFTER  UPDATE: both OLD and NEW, read-only
# BEFORE DELETE: only OLD, read-only
# AFTER  DELETE: only OLD, read-only

CREATE TRIGGER trg_balance_check
BEFORE UPDATE ON accounts
FOR EACH ROW
BEGIN
  IF NEW.balance < 0 THEN
    SIGNAL SQLSTATE '45000'
      SET MESSAGE_TEXT = 'Balance cannot be negative';
  END IF;
END;

多事件触发器 (FOLLOWS/PRECEDES)

自 5.7.2 起,一张表可以有多个相同事件和时序的触发器,用 FOLLOWS/PRECEDES 排序。每个触发器仍只处理一个事件(INSERT、UPDATE 或 DELETE)和一个时序(BEFORE/AFTER)——要处理多个事件需创建多个触发器。当触发器相互依赖时顺序很重要。MySQL 没有 INSTEAD OF 触发器(视图可直接更新)。

mysql
# MySQL allows multiple triggers with the same event/timing on one table (5.7.2+)
# order them with FOLLOWS / PRECEDES

DELIMITER //
CREATE TRIGGER trg_users_after_insert_2
AFTER INSERT ON users
FOR EACH ROW
FOLLOWS trg_users_after_insert
BEGIN
  UPDATE user_stats SET total = total + 1 WHERE user_id = NEW.id;
END //
DELIMITER ;

# events: INSERT | UPDATE | DELETE
# timing: BEFORE | AFTER
# a trigger cannot span multiple events; create one per event

管理触发器

SHOW TRIGGERS 列出所有触发器(注意 'Table' 列名因是保留字需反引号引用)。SHOW CREATE TRIGGER 返回完整触发器体和定义者。information_schema.triggers 提供结构化元数据用于审计。DROP TRIGGER IF EXISTS 对脚本安全。触发器绑定到表——删除表会删除其触发器。注意定义者:与过程一样,触发器默认以定义者权限运行。

mysql
# list triggers
SHOW TRIGGERS\G
SHOW TRIGGERS WHERE `Table` = 'users';

# show definition
SHOW CREATE TRIGGER trg_users_before_insert\G

# query metadata
SELECT trigger_name, event_manipulation, action_timing,
       event_object_table, created
FROM information_schema.triggers
WHERE trigger_schema = 'mydb';

# drop a trigger
DROP TRIGGER IF EXISTS trg_users_before_insert;

# triggers fire on the table's schema; definer is recorded
14

事务与锁

事务控制

MySQL 默认自动提交——每条语句是自己的事务。START TRANSACTION/BEGIN 开始显式块,以 COMMIT(保存)或 ROLLBACK(撤销)结束。只有事务引擎(InnoDB)支持事务;MyISAM 不支持。关键点:DDL 语句(CREATE/ALTER/DROP/TRUNCATE)会隐式提交当前事务且不可回滚,所以不要期望原子性地混合 DDL 和事务性 DML。

mysql
# explicit transaction (autocommit is on by default)
START TRANSACTION;
  UPDATE accounts SET balance = balance - 100 WHERE id = 1;
  UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;   -- or ROLLBACK;

# alternative syntax
BEGIN;
  INSERT INTO orders (user_id, amount) VALUES (1, 50);
  UPDATE users SET balance = balance - 50 WHERE id = 1;
COMMIT;

# turn autocommit off for the session
SET autocommit = 0;
-- every statement is now part of a transaction until COMMIT/ROLLBACK
SET autocommit = 1;

# DDL (CREATE/ALTER/DROP) implicitly commits — cannot be rolled back

保存点

SAVEPOINT 在事务内标记一个命名点,可以部分回滚到该点而不中止整个事务。ROLLBACK TO SAVEPOINT 撤销保存点之后的工作;RELEASE SAVEPOINT 丢弃标记(工作保留)。保存点让您尝试可选步骤并优雅恢复。它们在事务外不可见,在 COMMIT/ROLLBACK 时消失。支持嵌套保存点。

mysql
START TRANSACTION;
  INSERT INTO orders (amount) VALUES (10);
  SAVEPOINT sp_after_order;

  INSERT INTO order_items (order_id) VALUES (LAST_INSERT_ID());
  SAVEPOINT sp_after_items;

  -- something risky
  UPDATE inventory SET stock = stock - 1 WHERE sku='X';
  -- undo just the last step, keep the order + items
  ROLLBACK TO SAVEPOINT sp_after_items;

  -- release a savepoint when no longer needed
  RELEASE SAVEPOINT sp_after_order;
COMMIT;

隔离级别

隔离级别在一致性和并发性之间权衡。MySQL 默认 REPEATABLE READ 防止脏读和不可重复读,且不同于 SQL 标准,通过 next-key 锁定还避免了大部分幻读。READ COMMITTED 因更低锁定而流行但允许不可重复读。SERIALIZABLE 将所有读转为锁定读。InnoDB 使用 MVCC,除 SERIALIZABLE 外读不阻塞写。

mysql
# four levels, from least to most strict
# READ UNCOMMITTED  - dirty reads possible (rarely used)
# READ COMMITTED    - no dirty reads; non-repeatable reads possible
# REPEATABLE READ   - default; consistent reads within a transaction
# SERIALIZABLE      - locks everything; safest, slowest

# set for the session
SET SESSION transaction_isolation = 'REPEATABLE-READ';
# MySQL 8.0+ name; older: tx_isolation

# set globally (new connections inherit it)
SET GLOBAL transaction_isolation = 'READ-COMMITTED';

# inspect
SELECT @@transaction_isolation;
SELECT @@GLOBAL.transaction_isolation;

LOCK TABLES

LOCK TABLES 获取表级锁,主要是 MyISAM 时代的功能——InnoDB 的行级锁定通常更可取。READ 锁允许其他会话读但阻止写;WRITE 锁阻止其他所有人。持有锁时只能访问已锁定的表。LOCK TABLES 隐式提交任何活动事务,因此不与事务组合。InnoDB 并发控制优先使用 SELECT ... FOR UPDATE。

mysql
# explicit table locks (outside transactions; MyISAM-style)
LOCK TABLES users WRITE, orders READ;
  -- only these tables are accessible; users is write-locked
  SELECT * FROM users;
UNLOCK TABLES;

# READ  lock: this session can read; others can read but not write
# WRITE lock: only this session can read/write; others blocked entirely

# in InnoDB prefer row-level locks (SELECT FOR UPDATE) over LOCK TABLES
# LOCK TABLES implicitly commits the active transaction

SELECT ... FOR UPDATE / SHARE

SELECT ... FOR UPDATE 获取排他行锁,使其他事务在尝试更新或锁定同一行时阻塞——对转账等读-改-写序列至关重要。FOR SHARE 获取共享锁(其他可读但不可写)。锁在 COMMIT/ROLLBACK 时释放。8.0 新增 NOWAIT(出错而非等待)和 SKIP LOCKED(跳过锁定行)——非常适合不应相互阻塞的作业队列工作者。

mysql
START TRANSACTION;
  # lock the row so other transactions cannot modify it until commit
  SELECT balance INTO @b FROM accounts WHERE id = 1 FOR UPDATE;

  IF @b >= 100 THEN
    UPDATE accounts SET balance = balance - 100 WHERE id = 1;
  END IF;
COMMIT;   -- releases the lock

# FOR UPDATE: exclusive lock (write intent)
# FOR SHARE:  shared lock (read intent, others can read)

# NOWAIT / SKIP LOCKED reduce waiting (8.0+)
SELECT * FROM jobs WHERE status='pending' LIMIT 1 FOR UPDATE SKIP LOCKED;

死锁与检测

InnoDB 自动检测死锁并回滚一个事务(牺牲者)使另一个继续——您会收到错误 1213。SHOW ENGINE INNODB STATUS 显示最后一次死锁;启用 innodb_print_all_deadlocks 记录每一次。通过在事务间以一致顺序锁定行、保持事务简短、为锁定的列添加索引(否则锁升级为大范围)来预防死锁。始终为可能死锁的事务编写重试逻辑。

mysql
# deadlock: two transactions each hold a lock the other needs
# Tx1: locks A, wants B
# Tx2: locks B, wants A
# -> InnoDB detects it and rolls back the victim automatically

# see the last deadlock
SHOW ENGINE INNODB STATUS\G

# log all deadlocks to the error log
SET GLOBAL innodb_print_all_deadlocks = 1;

# mitigate by:
#  - locking tables/rows in a consistent order across transactions
#  - keeping transactions short
#  - adding appropriate indexes (avoid gap locks on full scans)
#  - using a lower isolation level where safe
15

用户管理

创建用户与认证

MySQL 账户始终是 'user'@'host'——host 控制用户从哪里连接;'%' 是通配任意主机。MySQL 8.0 默认使用 caching_sha2_password(更安全),某些旧客户端不支持——需回退到 mysql_native_password。优先使用范围更窄的 host(10.0.0.%)而非 '%'。CREATE USER 优于旧的 GRANT ... IDENTIFIED BY 语法(8.0 中已移除后者)。

mysql
# create a user that can connect from anywhere
CREATE USER 'app'@'%' IDENTIFIED BY 'StrongPass!2024';

# restrict to localhost only
CREATE USER 'admin'@'localhost' IDENTIFIED BY 'localpass';

# MySQL 8.0 default plugin is caching_sha2_password
CREATE USER 'dev'@'10.0.%.%' IDENTIFIED WITH caching_sha2_password
  BY 'devpass';

# legacy mysql_native_password (needed for some older clients)
CREATE USER 'legacy'@'%' IDENTIFIED WITH mysql_native_password
  BY 'legacypass';

# rename or change host
RENAME USER 'app'@'%' TO 'appuser'@'10.0.0.%';

# drop a user
DROP USER IF EXISTS 'legacy'@'%';

GRANT 授权

GRANT 在四个范围分配权限:全局(*.*)、数据库(db.*)、表(db.table)和列/例程。ALL PRIVILEGES 是除 GRANT OPTION 外的一切。使用所需最小权限——报表用户应只获得 SELECT。WITH GRANT OPTION 允许用户将自己的权限授予他人(危险)。FLUSH PRIVILEGES 很少需要,因为 GRANT 已更新内存表;仅在手动编辑 mysql 表后使用。

mysql
# grant all on a specific database
GRANT ALL PRIVILEGES ON mydb.* TO 'app'@'%';

# grant read-only on one table
GRANT SELECT ON mydb.users TO 'reporting'@'%';

# grant specific privileges
GRANT SELECT, INSERT, UPDATE ON mydb.* TO 'editor'@'localhost';

# grant the ability to grant to others (WITH GRANT OPTION)
GRANT SELECT ON mydb.* TO 'lead'@'%' WITH GRANT OPTION;

# grant at server level (use sparingly)
GRANT PROCESS, REPLICATION CLIENT ON *.* TO 'monitor'@'10.0.0.5';

# reload privilege tables into memory (usually automatic)
FLUSH PRIVILEGES;

REVOKE 撤销权限

REVOKE 移除先前授予的权限;必须精确匹配范围。要剥离包括 GRANT OPTION 在内的一切,使用 REVOKE ALL PRIVILEGES, GRANT OPTION。撤销不会删除用户——账户仍然存在且可以连接但在再次授权前无任何权限。用 SHOW GRANTS 检查用户的有效权限;更改访问前务必审查。完全删除用户用 DROP USER。

mysql
# remove specific privileges
REVOKE INSERT, UPDATE ON mydb.* FROM 'editor'@'localhost';

# remove all privileges (but keep the user)
REVOKE ALL PRIVILEGES, GRANT OPTION FROM 'app'@'%';

# revoke at a specific scope
REVOKE SELECT ON mydb.users FROM 'reporting'@'%';

# see what a user currently has
SHOW GRANTS FOR 'app'@'%';

# show grants for the current user
SHOW GRANTS;
SHOW GRANTS FOR CURRENT_USER();

角色 (MySQL 8.0+)

角色(8.0+)让您将权限分组并一次分配给多个用户,简化访问管理。被授予角色的用户在角色激活前不获得其权限——SET DEFAULT ROLE 使其在登录时默认激活。CURRENT_ROLE() 显示活动角色。更改一次角色,所有成员用户继承变更。这比向每个用户单独授予相同权限干净得多。

mysql
# a role is a named bundle of privileges
CREATE ROLE 'app_read', 'app_write';

GRANT SELECT ON mydb.* TO 'app_read';
GRANT INSERT, UPDATE, DELETE ON mydb.* TO 'app_write';

# grant a role to users
GRANT 'app_read' TO 'alice'@'%', 'bob'@'%';
GRANT 'app_write' TO 'carol'@'%';

# a user must activate a role to use it (default: none)
SET DEFAULT ROLE 'app_read' TO 'alice'@'%';
SET DEFAULT ROLE ALL TO 'alice'@'%';

# see active roles
SELECT CURRENT_ROLE();

密码管理

ALTER USER 是管理密码的现代方式(SET PASSWORD 已弃用)。PASSWORD EXPIRE 强制下次登录时重置——适用于密码轮换策略。validate_password 组件在 MEDIUM(大小写混合、数字、特殊字符)和 STRONG(还禁止字典词)级别强制长度/复杂度。ACCOUNT LOCK/UNLOCK 临时禁用账户而不删除——便于在调查期间暂停访问。

mysql
# change your own password
ALTER USER USER() IDENTIFIED BY 'NewPass!2024';

# change another user's password
ALTER USER 'app'@'%' IDENTIFIED BY 'NewAppPass!2024';

# require a password to be expired (user must reset on next login)
ALTER USER 'app'@'%' PASSWORD EXPIRE;

# enforce password policy (8.0+ validate_password component)
INSTALL COMPONENT 'file://component_validate_password';
SET GLOBAL validate_password.policy = 'MEDIUM';  -- 0/LOW,1/MEDIUM,2/STRONG
SET GLOBAL validate_password.length = 12;

# lock an account temporarily
ALTER USER 'app'@'%' ACCOUNT LOCK;
ALTER USER 'app'@'%' ACCOUNT UNLOCK;

显示授权与用户

mysql.user 是保存所有账户的系统表——查询它来清点用户和标志如 account_locked 和 password_expired。SHOW GRANTS 是更友好的解码视图,展示一个用户的权限。CURRENT_USER() 是用于权限检查的身份(可能与连接字符串 USER() 不同)——通过角色或代理连接时很重要。定期审计这些以查找休眠或过度授权的账户。

mysql
# list all users (MySQL 8.0+)
SELECT user, host, account_locked, password_expired
FROM mysql.user;

# grants for a specific user
SHOW GRANTS FOR 'app'@'%';

# grants for the current session
SHOW GRANTS;
SHOW GRANTS FOR CURRENT_USER();

# who am I?
SELECT CURRENT_USER(), USER();

# active roles for the current session
SELECT CURRENT_ROLE();

# list global privileges
SELECT user, host, Super_priv, Process_priv, Reload_priv
FROM mysql.user;
16

备份与恢复

mysqldump

mysqldump 是经典的逻辑备份工具——它输出重新创建数据库的 SQL 语句。--single-transaction 通过使用单个 REPEATABLE READ 事务提供一致的 InnoDB 快照而无需锁定。--routines/--triggers/--events 包含存储程序(否则省略)。大型数据库优先使用 Percona XtraBackup 或 MySQL Enterprise Backup(物理备份),比 mysqldump 快得多。

mysql
# dump a single database to a file
mysqldump -u root -p mydb > mydb.sql

# dump all databases
mysqldump -u root -p --all-databases > alldb.sql

# dump with routines, triggers and events
mysqldump -u root -p --routines --triggers --events mydb > mydb_full.sql

# dump schema only (no data) or data only
mysqldump -u root -p --no-data mydb > schema.sql
mysqldump -u root -p --no-create-info mydb > data.sql

# consistent dump of InnoDB in one transaction
mysqldump -u root -p --single-transaction mydb > mydb.sql

# compress on the fly
mysqldump -u root -p mydb | gzip > mydb.sql.gz

从转储恢复

恢复时将转储文件管道到 mysql 客户端或在其中使用 SOURCE。目标数据库必须已存在(先 CREATE DATABASE 再 USE)。为加快恢复,在加载期间禁用外键和唯一性检查以及自动提交,然后重新启用——转储本身通常已包含这些保护。用 gunzip 恢复压缩转储避免解压到磁盘。始终在暂存服务器上测试恢复。

mysql
# restore a database dump (database must exist)
mysql -u root -p mydb < mydb.sql

# restore all databases (creates them)
mysql -u root -p < alldb.sql

# restore a compressed dump
gunzip < mydb.sql.gz | mysql -u root -p mydb

# restore from inside the mysql client
SOURCE /path/to/mydb.sql;

# speed up restores: disable checks and autocommit during load
SET foreign_key_checks = 0;
SET unique_checks = 0;
SET autocommit = 0;
SOURCE /path/to/mydb.sql;
COMMIT;
SET foreign_key_checks = 1;
SET unique_checks = 1;

导出为 CSV

SELECT ... INTO OUTFILE 写入受 secure_file_priv 目录限制的服务器端文件(若 secure_file_priv 为 NULL 则完全禁用),需要 FILE 权限——不能覆盖已存在文件。mysql 客户端上的 --batch --raw 在本地生成制表符分隔文件,无这些限制,适合 cron 作业。FIELDS/LINES 子句控制 CSV 兼容的分隔符、引号和转义。

mysql
# server-side export to a file (needs FILE privilege + secure_file_priv)
SELECT id, username, email, created_at
FROM users
INTO OUTFILE '/var/lib/mysql-files/users.csv'
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
ESCAPED BY '\\'
LINES TERMINATED BY '\n';

# client-side export with the mysql client (no FILE privilege needed)
mysql -u root -p -e "SELECT id, username, email FROM users" \
  --batch --raw mydb > users.tsv

# include column headers with a UNION
SELECT 'id','username','email' UNION
SELECT id, username, email FROM users
INTO OUTFILE '/var/lib/mysql-files/users.csv'
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"';

从 CSV 导入

LOAD DATA INFILE 是加载 CSV 最快的方式——远快于 INSERT 语句。服务器端形式从 secure_file_priv 下的路径读取;LOCAL 读取客户端文件(需在客户端和服务器上启用 local_infile)。IGNORE 1 LINES 跳过表头。使用 @var 捕获列再 SET 转换值(如解析日期字符串)。超大批量加载请禁用键/外键并使用单个事务。

mysql
# server-side import (needs FILE privilege)
LOAD DATA INFILE '/var/lib/mysql-files/users.csv'
INTO TABLE users
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 LINES
(id, username, email, @created_at)
SET created_at = STR_TO_DATE(@created_at, '%Y-%m-%d %H:%i:%s');

# client-side import (no FILE privilege, sends file from client)
LOAD DATA LOCAL INFILE 'C:/data/users.csv'
INTO TABLE users
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\r\n'
IGNORE 1 LINES (id, username, email);

# enable LOCAL on the client:  --local-infile=1
# and on the server:  SET GLOBAL local_infile = 1;

二进制日志 (mysqlbinlog)

二进制日志记录所有数据变更语句用于复制和时间点恢复。SHOW MASTER STATUS 给出当前文件和位置(复制坐标)。mysqlbinlog 将 binlog 文件转回 SQL(或行事件)以检查或重放——配合开始/停止时间或位置恢复到精确时刻。PURGE BINARY LOGS 回收磁盘;让 expire_logs_days/binlog_expire_logs_seconds 自动化。

mysql
# enable the binary log (my.cnf)
# [mysqld]
# log_bin = mysql-bin
# binlog_format = ROW
# server_id = 1

# show current binlog files and position
SHOW BINARY LOGS;
SHOW MASTER STATUS;

# view events in a binlog file
mysqlbinlog mysql-bin.000123

# filter by time / position
mysqlbinlog --start-datetime="2024-06-01 00:00:00" \
            --stop-datetime="2024-06-01 12:00:00" \
            mysql-bin.000123 > replay.sql

# replay events to recover
mysql -u root -p < replay.sql

# purge old binlogs
PURGE BINARY LOGS BEFORE '2024-06-01 00:00:00';

克隆插件与复制

克隆插件(8.0+)物理复制 InnoDB 数据目录,比逻辑转储快得多,适合配置副本或快照。复制时用 CHANGE REPLICATION SOURCE TO(8.0+;原为 CHANGE MASTER TO)将副本指向源,再 START REPLICA。GTID(SOURCE_AUTO_POSITION=1)使复制定位更健壮。SHOW REPLICA STATUS 揭示延迟和错误。监控 Seconds_Behind_Source 了解副本健康状况。

mysql
# install the clone plugin (8.0+) for fast physical copy
INSTALL PLUGIN clone SONAME 'mysql_clone.so';

# clone a local data directory to another path
CLONE LOCAL DATA DIRECTORY = '/var/lib/mysql-clone';

# clone from a remote donor (good for provisioning replicas)
CLONE INSTANCE FROM 'donor'@'10.0.0.2':3306
  IDENTIFIED BY 'donorpass';

# set up replication from a donor
CHANGE REPLICATION SOURCE TO
  SOURCE_HOST='10.0.0.2', SOURCE_PORT=3306,
  SOURCE_USER='repl', SOURCE_PASSWORD='replpass',
  SOURCE_AUTO_POSITION=1;
START REPLICA;   -- 8.0+ (was START SLAVE)
SHOW REPLICA STATUS\G
17

性能优化

EXPLAIN

EXPLAIN 揭示查询计划:使用哪个索引、估计扫描多少行、是否需要文件排序或临时表。'type' 列排列访问方法——const/eq_ref 最佳(单行查找),ref/range 使用索引,index 扫描整个索引,ALL 扫描整表(通常不好)。'Using index' 表示仅索引覆盖扫描;'Using filesort'/'Using temporary' 警告有昂贵操作需优化。

mysql
# show the execution plan for a query
EXPLAIN SELECT * FROM users WHERE email = '[email protected]';

# extended: extra info + warnings
EXPLAIN EXTENDED SELECT ...;
SHOW WARNINGS;

# format as a tree (8.0+) — more readable
EXPLAIN FORMAT=TREE SELECT ...;

# the key columns to read:
#   type        - access method (const > eq_ref > ref > range > index > ALL)
#   key         - the index chosen
#   rows        - estimated rows scanned
#   Extra       - 'Using index' (good) / 'Using filesort' / 'Using temporary'

EXPLAIN ANALYZE (8.0+)

EXPLAIN ANALYZE(8.0.17+)实际执行查询并报告每个迭代器的真实行数和耗时,暴露时间真正花在哪里——远比普通 EXPLAIN 的估计准确。在暂存环境对 SELECT 查询使用。配合 SHOW WARNINGS 查看优化器重写后的查询(视图合并、常量折叠后)。写查询用 EXPLAIN 检查只读等价物或用 performance_schema 检查。

mysql
# actually runs the query and reports real per-step timing
EXPLAIN ANALYZE
SELECT u.username, COUNT(o.id)
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id;

# output shows actual rows, loops and per-iterator ms:
# -> Table scan on users  (actual rows=1000, loops=1)
#     -> Covering index lookup on o  (actual rows=2.4, loops=1000)

# unlike EXPLAIN, this executes the statement (read-only queries only)

# also useful: SHOW WARNINGS after EXPLAIN to see the rewritten query
EXPLAIN SELECT ...;
SHOW WARNINGS\G

优化器提示

优化器提示(/*+ ... */)为单条语句覆盖计划器。比 SET GLOBAL 更精准且在语句重启后仍然有效。用它们修复糟糕的计划,但也要处理根本原因(过时统计、缺失索引、数据倾斜)——提示会随数据变化而退化。MAX_EXECUTION_TIME 限制只读查询,保护服务器免受失控 SELECT 影响。SET_VAR 仅为一条语句调整会话变量。

mysql
# index hints (8.0+ style, after SELECT)
SELECT /*+ INDEX(u idx_email) */ *
FROM users u
WHERE email = '[email protected]';

SELECT /*+ NO_RANGE_OPTIMIZATION(u) */ *
FROM users u WHERE id BETWEEN 1 AND 1000;

# join order / algorithm hints
SELECT /*+ JOIN_ORDER(a,b,c) */ * FROM a JOIN b ON ... JOIN c ON ...;
SELECT /*+ HASH_JOIN(b) */ * FROM a JOIN b ON a.id=b.aid;
SELECT /*+ NLJ(b) */ * FROM a JOIN b ON a.id=b.aid;

# resource hints
SELECT /*+ MAX_EXECUTION_TIME(1000) */ * FROM big_table;
# aborts the query after 1000 ms (read-only)

# SET_VAR hint changes a variable for one statement
SELECT /*+ SET_VAR(sort_buffer_size=16M) */ ... FROM ...;

慢查询日志

慢查询日志捕获超过 long_query_time(秒,允许亚秒)的查询。启用 log_queries_not_using_indexes 捕获即使很快的全表扫描。mysqldumpslow 聚合相似查询并按时间/计数/行数排序,以便优先处理最差者。Percona 的 pt-query-digest 提供更丰富报告(指纹、百分位)——慢日志分析的标准工具。定期轮转和清理日志。

mysql
# enable in my.cnf
# [mysqld]
# slow_query_log = 1
# slow_query_log_file = /var/log/mysql/slow.log
# long_query_time = 2          # log queries slower than 2s
# log_queries_not_using_indexes = 1
# log_slow_admin_statements = 1

# enable at runtime (persists until restart)
SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 1;

# analyze the slow log with mysqldumpslow
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
#   -s t sort by total time, -t 10 top 10
#   -s c count, -s r rows, -s l lock time

# pt-query-digest (Percona Toolkit) gives richer analysis
pt-query-digest /var/log/mysql/slow.log

SHOW PROFILE 与 Performance Schema

SHOW PROFILE 将查询分解为阶段(打开表、排序、发送数据)——方便但已弃用。现代等价物是 performance_schema,8.0 默认启用的低开销检测框架。查询 events_statements_summary_by_digest 找到服务器上消耗最多聚合时间的查询,然后深入 events_stages_history_long 获取每阶段细节。这是找到系统性瓶颈而非单个慢查询的方法。

mysql
# profiling (deprecated but quick) - per-statement stage timings
SET profiling = 1;
SELECT COUNT(*) FROM huge_table;
SHOW PROFILE;
SHOW PROFILE FOR QUERY 1;

# preferred: performance_schema (8.0+, enabled by default)
# top queries by total latency
SELECT digest_text, count_star, sum_timer_wait/1e9 AS total_s
FROM performance_schema.events_statements_summary_by_digest
ORDER BY sum_timer_wait DESC LIMIT 10;

# which stage of which query took longest
SELECT event_name, format_time(timer_wait) AS took
FROM performance_schema.events_stages_history_long
ORDER BY timer_wait DESC LIMIT 10;

索引优化技巧

索引设计赢得大多数性能战役。遵循最左前缀规则,避免前导通配符,不要用函数包裹索引列(或使用生成列 + 索引)。热查询争取覆盖索引。保持 WHERE 条件可搜索参数化(sargable)以使用索引:created_at >= x AND created_at < y 优于 DATE(created_at)=x。ANALYZE TABLE 刷新优化器依赖的基数统计。

mysql
# 1. index columns used in WHERE / JOIN / ORDER BY
#    with the most selective equality column first
CREATE INDEX idx_user_status ON orders(user_id, status, created_at);

# 2. avoid leading wildcards (defeats the index)
SELECT * FROM users WHERE name LIKE 'A%';   -- OK
SELECT * FROM users WHERE name LIKE '%A';   -- full scan

# 3. don't wrap indexed columns in functions
SELECT * FROM orders WHERE DATE(created_at)='2024-01-01';        -- bad
SELECT * FROM orders WHERE created_at >= '2024-01-01'
  AND created_at < '2024-01-02';                                  -- good

# 4. use a covering index so the query is index-only
EXPLAIN SELECT user_id, status FROM orders WHERE user_id=5;
-- 'Using index' in Extra means no table lookup

# 5. refresh statistics after big loads
ANALYZE TABLE orders;
18

JSON 操作

JSON 列与插入

JSON 类型以二进制验证格式存储 JSON。用 JSON 字符串字面量插入或用 JSON_OBJECT()(键/值对)和 JSON_ARRAY() 构建值。无效 JSON 在插入时被拒绝。因为 JSON 存储为解析树,成员访问很快,但整个文档一起读取——对于频繁查询的标量值,将其提取到生成列并建索引以获得两全其美。

mysql
CREATE TABLE products (
  id    INT PRIMARY KEY,
  name  VARCHAR(100),
  attrs JSON
);

# insert JSON literals
INSERT INTO products VALUES
  (1, 'Widget',
   '{"color":"red","size":42,"tags":["new","sale"],"stock":{"ny":5,"la":3}}'),
  (2, 'Gadget', '{"color":"blue","in_stock":true}');

# build JSON with functions
INSERT INTO products VALUES (3, 'Thing',
  JSON_OBJECT('color','green','tags',JSON_ARRAY('new')));

JSON_EXTRACT / -> / ->>

-> 提取 JSON 成员(返回带引号的 JSON);->> 返回不带引号的文本(比较时的常用选择)。路径使用 $. 表示根,$.key 表示成员,$.arr[0] 表示数组索引,$.a.b 表示嵌套。JSON_CONTAINS_PATH 检查键是否存在。JSON_EXTRACT 是 -> 的函数形式。要按成员值高效查找文档,在 attrs->>'$.color' 上添加生成列 + 索引。

mysql
# extract a member (returns JSON)
SELECT JSON_EXTRACT(attrs, '$.color') FROM products WHERE id=1;   -- "red"
SELECT attrs->'$.color'   FROM products WHERE id=1;               -- "red"

# extract as text (unquoted)
SELECT attrs->>'$.color'  FROM products WHERE id=1;               -- red

# array element access
SELECT attrs->>'$.tags[0]' FROM products WHERE id=1;              -- new

# nested paths
SELECT attrs->>'$.stock.ny' FROM products WHERE id=1;             -- 5

# filter rows by a JSON value
SELECT id, name FROM products
WHERE attrs->>'$.color' = 'red';

# exists check
SELECT id FROM products
WHERE JSON_CONTAINS_PATH(attrs, 'one', '$.stock');

JSON_SET / INSERT / REPLACE / REMOVE

这些函数返回修改后的 JSON 文档——必须将结果赋回以更新列。JSON_SET 插入或更新;INSERT 仅添加缺失路径;REPLACE 仅更新已存在路径;REMOVE 删除。JSON_MERGE_PATCH(RFC 7396)合并两个文档并删除设为 null 的键——应用部分补丁的标准方式。JSON_MERGE_PRESERVE 保留而非替换现有数组/对象。

mysql
# SET updates or adds a member (returns the modified document)
UPDATE products
SET attrs = JSON_SET(attrs, '$.color', 'green', '$.weight', 1.2)
WHERE id = 1;

# INSERT adds only if the path doesn't exist
UPDATE products
SET attrs = JSON_INSERT(attrs, '$.code', 'W-100')
WHERE id = 1;

# REPLACE updates only if the path exists
UPDATE products
SET attrs = JSON_REPLACE(attrs, '$.size', 99)
WHERE id = 1;

# REMOVE deletes a member
UPDATE products
SET attrs = JSON_REMOVE(attrs, '$.tags[0]')
WHERE id = 1;

# MERGE_PATCH merges two JSON documents (8.0+)
SET attrs = JSON_MERGE_PATCH(attrs, '{"size":50,"color":null}')

JSON_TABLE (8.0+)

JSON_TABLE(8.0+)是从 JSON 到关系型的桥梁——它将 JSON 文档拆分为可像任何表一样连接、过滤和聚合的行。columns 子句将 JSON 路径映射到有类型的列。NESTED PATH 在一遍中展开嵌套数组。这使 JSON 列适用于半结构化数据:存储灵活 JSON,需要 SQL 分析时用 JSON_TABLE 拆分为关系型,并为热路径索引生成列。

mysql
# turn a JSON array of objects into relational rows
SELECT jt.*
FROM products p,
JSON_TABLE(p.attrs, '$.tags[*]'
  COLUMNS (
    tag VARCHAR(50) PATH '$'  -- each element as a column
  )
) AS jt
WHERE p.id = 1;

# flatten an array of objects
SELECT p.id, jt.color, jt.size
FROM products p,
JSON_TABLE(p.attrs, '$'
  COLUMNS (
    color VARCHAR(20) PATH '$.color',
    size  INT          PATH '$.size'
  )
) AS jt;

# nested arrays: NESTED PATH
JSON_TABLE(j, '$.stock.*'
  COLUMNS (warehouse VARCHAR(5) PATH '$.wh', qty INT PATH '$.qty'))

JSON 聚合函数

JSON_ARRAYAGG 将每组的值收集为 JSON 数组;JSON_OBJECTAGG 构建 key->value 对象(后出现的键覆盖先前的重复键)。配合 JSON_OBJECT 可直接在 SQL 中构建对象数组用于 API 响应——便于组装嵌套载荷而无需应用层后处理。两者都跳过 NULL 输入。注意结果大小:超大分组会产生超大 JSON 值。

mysql
# JSON_ARRAYAGG: collect values into a JSON array
SELECT user_id,
  JSON_ARRAYAGG(email) AS emails
FROM contacts
GROUP BY user_id;
-- ["[email protected]","[email protected]"]

# JSON_OBJECTAGG: key/value pairs into a JSON object
SELECT user_id,
  JSON_OBJECTAGG(type, value) AS prefs
FROM user_prefs
GROUP BY user_id;
-- {"theme":"dark","lang":"en"}

# build a JSON array of objects with JSON_OBJECT + ARRAYAGG
SELECT JSON_ARRAYAGG(
  JSON_OBJECT('id', id, 'name', username)
) AS users
FROM users WHERE status='active';

JSON 验证与 Schema

JSON_VALID 检查格式良好性(插入时已强制执行)。JSON_PRETTY 重新格式化以提高可读性;JSON_STORAGE_SIZE 报告字节数。JSON_SCHEMA_VALID(8.0.17+)让 CHECK 约束强制执行 JSON Schema(必需键、类型)——在不失灵活性的前提下为 JSON 列赋予结构的轻量方式。JSON_TABLE 和 JSON_EXTRACT 接受 ON EMPTY / ON ERROR 子句控制路径缺失或无效时的行为。

mysql
# validate a string is JSON
SELECT JSON_VALID('{"a":1}');          -- 1
SELECT JSON_VALID('{bad}');            -- 0

# pretty-print and minify
SELECT JSON_PRETTY(attrs) FROM products WHERE id=1;
SELECT JSON_STORAGE_SIZE(attrs) FROM products WHERE id=1;  -- bytes

# JSON_SCHEMA_VALID (8.0.17+) enforces a schema with CHECK
CREATE TABLE events (
  id INT PRIMARY KEY,
  data JSON,
  CHECK (JSON_SCHEMA_VALID(
    '{"type":"object","required":["ts","type"],
      "properties":{"ts":{"type":"string"},
                    "type":{"type":"string"}}}', data))
);

# JSON_TABLE with ERROR ON ERROR for strict validation
SELECT * FROM JSON_TABLE(j, '$' COLUMNS(v INT PATH '$.x')
  DEFAULT NULL ON EMPTY ERROR ON ERROR) AS t;
19

窗口函数

ROW_NUMBER()

ROW_NUMBER() 在分区内按指定顺序为每行分配唯一连续整数——排名、去重和每组取 Top-N 的标准方法。与 RANK/DENSE_RANK 不同,它从不产生并列。通过 CTE 的 DELETE 模式删除重复项同时保留每个键一行(不能直接从 CTE 删除,所以先选择要删除的 ID)。窗口函数需要 MySQL 8.0+。

mysql
# unique sequential number per row within a partition
SELECT username, country,
  ROW_NUMBER() OVER (PARTITION BY country ORDER BY created_at) AS rn
FROM users;

# top 3 oldest users per country
WITH ranked AS (
  SELECT *,
    ROW_NUMBER() OVER (PARTITION BY country ORDER BY birth_date) AS rn
  FROM users
)
SELECT * FROM ranked WHERE rn <= 3;

# deduplicate: keep the latest row per email
WITH ranked AS (
  SELECT *,
    ROW_NUMBER() OVER (PARTITION BY email ORDER BY id DESC) AS rn
  FROM users
)
DELETE FROM users WHERE id IN (
  SELECT id FROM ranked WHERE rn > 1
);

RANK() 与 DENSE_RANK()

RANK() 给并列相同排名并跳过后续排名(1,1,4,5);DENSE_RANK() 给并列相同排名且不跳过(1,1,3,4);ROW_NUMBER() 从不并列(1,2,3,4)。按意图选择:排行榜式排名通常用 RANK 或 DENSE_RANK。DENSE_RANK 是查找每组第 N 高(rank=2 返回第二个不同薪资)的经典方法。三者共享相同的 OVER() 语法。

mysql
# all three ranking functions on the same data
SELECT name, score,
  ROW_NUMBER() OVER (ORDER BY score DESC) AS rn,
  RANK()       OVER (ORDER BY score DESC) AS rnk,
  DENSE_RANK() OVER (ORDER BY score DESC) AS drnk
FROM players;

# with scores 100,100,90,80 the functions return:
#   rn: 1,2,3,4         (always sequential)
#   rnk:1,2,4,5         (ties share a rank, next rank skips)
#   drnk:1,2,3,4        (ties share a rank, no gap)

# find the 2nd-highest salary per department
WITH ranked AS (
  SELECT dept, salary,
    DENSE_RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS dr
  FROM employees
)
SELECT DISTINCT dept, salary FROM ranked WHERE dr = 2;

LAG() 与 LEAD()

LAG() 和 LEAD() 查看相对于当前行的另一行——LAG 向后看,LEAD 向前看——无需自连接。它们接受可选偏移量(默认 1)和偏移超出分区时的默认值(默认 NULL)。非常适合周期对比差异、移动比较和检测序列中的连续/中断。始终指定 ORDER BY 使'前一行'定义明确。

mysql
# compare each row to the previous/next row
SELECT day, sales,
  LAG(sales)  OVER (ORDER BY day) AS prev_day,
  sales - LAG(sales) OVER (ORDER BY day) AS diff,
  LEAD(sales) OVER (ORDER BY day) AS next_day
FROM daily_sales;

# with an explicit offset and default value
SELECT day, sales,
  LAG(sales, 7, 0) OVER (ORDER BY day) AS same_day_last_week
FROM daily_sales;

# year-over-year comparison
SELECT year, region, revenue,
  LAG(revenue) OVER (PARTITION BY region ORDER BY year) AS prev_year,
  revenue - LAG(revenue) OVER (PARTITION BY region ORDER BY year) AS yoy
FROM annual_revenue;

窗口聚合 (SUM / AVG OVER)

带 OVER() 的聚合函数在移动行窗口上计算聚合而非折叠行。无帧时 SUM OVER (ORDER BY x) 产生累计总和;带 ROWS BETWEEN 6 PRECEDING AND CURRENT ROW 是 7 行移动平均。空 OVER() 计算所有行总计。PARTITION BY 按组重置窗口。帧使用 ROWS(物理)或 RANGE(基于值)边界。

mysql
# running total over time
SELECT day, sales,
  SUM(sales) OVER (ORDER BY day) AS running_total
FROM daily_sales;

# moving average over a 7-day window
SELECT day, sales,
  AVG(sales) OVER (
    ORDER BY day
    ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
  ) AS ma7
FROM daily_sales;

# each row's share of its group total
SELECT user_id, amount,
  amount / SUM(amount) OVER (PARTITION BY user_id) AS share
FROM orders;

# grand total alongside detail rows
SELECT username, balance,
  SUM(balance) OVER () AS total_balance
FROM users;

FIRST_VALUE / LAST_VALUE / NTH_VALUE

FIRST_VALUE/NTH_VALUE 返回帧内特定行的值。LAST_VALUE 是常见陷阱:默认帧是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,所以'最后'意味着当前行而非分区的最后——将帧扩展到 UNBOUNDED FOLLOWING 才能获得真正的最后值。NTH_VALUE 在第 N 行出现前返回 NULL。显式帧消除歧义,值得始终指定。

mysql
# first value in each partition (per ordering)
SELECT day, region, sales,
  FIRST_VALUE(sales) OVER (PARTITION BY region ORDER BY day) AS first_sale
FROM daily_sales;

# LAST_VALUE needs an explicit frame to include current row!
SELECT day, region, sales,
  LAST_VALUE(sales) OVER (
    PARTITION BY region ORDER BY day
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS latest_so_far
FROM daily_sales;

# the Nth value within a partition
SELECT day, region, sales,
  NTH_VALUE(sales, 3) OVER (
    PARTITION BY region ORDER BY day
    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
  ) AS third
FROM daily_sales;

NTILE 与 PERCENT_RANK

NTILE(n) 将有序行尽可能均等地分成 n 个桶(不整除时前面的桶多一行)——适合报表中的四分位/十分位。PERCENT_RANK() 是 0..1 的相对排名分数;CUME_DIST() 是累积分布(小于等于当前值的行比例)。NTILE 用于分桶,PERCENT_RANK/CUME_DIST 用于百分位式分析。

mysql
# divide rows into N equal buckets (1..N)
SELECT name, salary,
  NTILE(4) OVER (ORDER BY salary DESC) AS quartile
FROM employees;
-- quartile 1 = top 25%, quartile 4 = bottom 25%

# relative rank as a fraction 0..1
SELECT name, salary,
  PERCENT_RANK() OVER (ORDER BY salary) AS pct,
  CUME_DIST()    OVER (ORDER BY salary) AS cum
FROM employees;
-- pct  = (rank-1)/(N-1), 0 = lowest, 1 = highest
-- cum  = (rows up to and including current)/N

# CUME_DIST: fraction of rows with a value <= current
20

日期时间处理

当前日期/时间函数

NOW()/CURRENT_TIMESTAMP 在整个语句中返回相同值(一致);SYSDATE() 反映每次调用的真实时间(避免使用——它不确定且破坏复制)。以 UTC 存储时间戳(UTC_TIMESTAMP)并在显示时转换为本地时间。NOW(6) 提供微秒精度。UNIX_TIMESTAMP 与 epoch 秒互转——便于与使用 epoch 时间的语言互操作。

mysql
# current timestamp (date + time) in one call
SELECT NOW(), SYSDATE(), CURRENT_TIMESTAMP, LOCALTIME;

# current date / time separately
SELECT CURDATE(), CURRENT_DATE;     -- '2024-07-02'
SELECT CURTIME(), CURRENT_TIME;     -- '14:30:00'

# UTC equivalents (store UTC, display local)
SELECT UTC_TIMESTAMP(), UTC_DATE(), UTC_TIME();

# microsecond precision with (6)
SELECT NOW(6), CURRENT_TIMESTAMP(6);

# UNIX timestamp (seconds since epoch) and back
SELECT UNIX_TIMESTAMP();                 -- 1719912600
SELECT FROM_UNIXTIME(1719912600);        -- '2024-07-02 12:30:00'
SELECT FROM_UNIXTIME(1719912600, '%Y-%m-%d');

DATE_FORMAT 与 STR_TO_DATE

DATE_FORMAT 使用说明符将日期/时间格式化为字符串——注意小写 %i(分钟)和 %s(秒),这是常见易错点。STR_TO_DATE 是逆操作,将字符串解析为 DATE/TIME/DATETIME。解析用户输入始终使用 STR_TO_DATE 而非依赖隐式字符串转日期转换,后者依赖格式设置且脆弱。日期应存储为 DATE/DATETIME 类型而非字符串。

mysql
# format a date/time to a string
SELECT DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s');   -- 2024-07-02 14:30:00
SELECT DATE_FORMAT(NOW(), '%d/%m/%Y');            -- 02/07/2024
SELECT DATE_FORMAT(NOW(), '%W, %M %e %Y');        -- Tuesday, July 2 2024

# parse a string into a date
SELECT STR_TO_DATE('02/07/2024', '%d/%m/%Y');     -- 2024-07-02
SELECT STR_TO_DATE('Jul 2, 2024 2:30 PM', '%b %e, %Y %h:%i %p');

# common specifiers:
#   %Y 4-digit year   %y 2-digit year
#   %m month(01-12)   %c month(1-12)   %M month name   %b abbreviated
#   %d day(01-31)     %e day(1-31)     %j day of year
#   %H hour(00-23)    %h hour(01-12)   %i minutes  %s seconds
#   %W weekday name   %a abbreviated   %p AM/PM

DATE_ADD / DATE_SUB 与 INTERVAL

DATE_ADD/DATE_SUB 配合 INTERVAL 是规范的日期运算;+ INTERVAL / - INTERVAL 简写更简洁。INTERVAL 正确处理月/年跨年(闰年 1 月 31 日 + 1 个月 = 2 月 29 日)。LAST_DAY 返回参数月份的最后一天——便于月末逻辑。复合单位如 YEAR_MONTH 取 '1-2'(1 年 2 个月)。绝不要用字符串运算处理日期——它在边缘情况下会出错。

mysql
# add an interval to a date
SELECT DATE_ADD('2024-01-31', INTERVAL 1 MONTH);   -- 2024-02-29
SELECT DATE_ADD('2024-01-31', INTERVAL 1 DAY);     -- 2024-02-01
SELECT '2024-01-31' + INTERVAL 1 MONTH;            -- shorthand

# subtract
SELECT DATE_SUB(NOW(), INTERVAL 7 DAY);
SELECT NOW() - INTERVAL 1 HOUR;

# interval units: MICROSECOND SECOND MINUTE HOUR DAY WEEK MONTH QUARTER YEAR
#   SECOND_MICROSECOND MINUTE_MICROSECOND ... YEAR_MONTH (compound)

# add to a datetime (keeps the time component)
SELECT DATE_ADD(NOW(), INTERVAL 30 MINUTE);

# last day of next month
SELECT LAST_DAY(DATE_ADD(CURDATE(), INTERVAL 1 MONTH));

DATEDIFF / TIMESTAMPDIFF

DATEDIFF 返回整天数(date1 - date2,忽略时间)。TIMESTAMPDIFF 返回所选单位的差值(unit, from, to)并截断向零——所以 TIMESTAMPDIFF(YEAR, ...) 仅在生日已过时给出年龄;精确年龄在月份/日期未到时减 1。注意参数顺序:DATEDIFF 是 (a,b) = a-b;TIMESTAMPDIFF 是 (unit, from, to) = to-from。

mysql
# DATEDIFF: difference in DAYS (date1 - date2)
SELECT DATEDIFF('2024-12-31', '2024-01-01');   -- 365
SELECT DATEDIFF(NOW(), birth_date) AS days_alive FROM users;

# TIMESTAMPDIFF: difference in a chosen unit (date1, date2) -> date2 - date1
SELECT TIMESTAMPDIFF(YEAR, birth_date, CURDATE()) AS age;
SELECT TIMESTAMPDIFF(MONTH, '2024-01-15', '2024-07-02');
SELECT TIMESTAMPDIFF(MICROSECOND, '2024-07-02 09:00:00', NOW());

# units: MICROSECOND SECOND MINUTE HOUR DAY WEEK MONTH QUARTER YEAR
# (TIMESTAMPDIFF argument order is (unit, from, to))

# TIME values: TIMEDIFF returns a TIME
SELECT TIMEDIFF('18:00:00', '09:30:00');   -- 08:30:00

EXTRACT 与日期部分

EXTRACT(unit FROM date) 返回单个数字分量;YEAR()/MONTH()/DAY() 等是常用部分的简写;DAYNAME/MONTHNAME 返回名称。按时间戳的 YEAR()/MONTH() 分组是构建月度报表的标准方式。注意 DAYOFWEEK 是 1=周日..7=周六,而 WEEKDAY 是 0=周一..6=周日——选择符合您约定的。WEEK() 有周起始日和计数模式。

mysql
# pull out a single component
SELECT EXTRACT(YEAR FROM created_at)   AS yr,
       EXTRACT(MONTH FROM created_at)  AS mon,
       EXTRACT(DAY FROM created_at)    AS dy
FROM orders;

# dedicated functions for each part
SELECT YEAR(created_at), MONTH(created_at), DAY(created_at),
       HOUR(created_at), MINUTE(created_at), SECOND(created_at),
       DAYOFWEEK(created_at), DAYOFYEAR(created_at),
       WEEK(created_at), QUARTER(created_at);

# names instead of numbers
SELECT DAYNAME(created_at), MONTHNAME(created_at);

# group orders by month for a report
SELECT YEAR(created_at) AS yr, MONTH(created_at) AS mon,
       COUNT(*) AS orders, SUM(amount) AS revenue
FROM orders
GROUP BY yr, mon
ORDER BY yr, mon;

时区转换

设置会话 time_zone 使 TIMESTAMP 列以本地时间显示;命名时区(Asia/Shanghai)需通过 mysql_tzinfo_to_sql 加载系统时区表。CONVERT_TZ 在时区间转换字面日期时间。最佳实践:所有时间以 UTC 存储(用 TIMESTAMP 或 UTC DATETIME),为显示设置每个会话的 time_zone,需要时用 CONVERT_TZ 转换。这避免了不同地区用户的歧义。

mysql
# see the session time zone
SELECT @@session.time_zone, @@global.time_zone;

# set the session time zone (UTC offset or named, needs tz data loaded)
SET time_zone = '+08:00';            # Asia/Shanghai offset
SET time_zone = 'Asia/Shanghai';     # named zone (load tz tables first)

# convert a datetime between time zones
SELECT CONVERT_TZ('2024-07-02 09:00:00', '+00:00', '+08:00');
-- 2024-07-02 17:00:00

# named zones require the timezone tables to be loaded:
#   mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root -p mysql

# TIMESTAMP columns auto-convert to the session time zone on display;
# DATETIME columns do not -- they store the literal value

# store all times in UTC and convert on the way out
SELECT CONVERT_TZ(created_at, '+00:00', @@session.time_zone)
FROM orders;

这篇内容对您有帮助吗?