本文档详细介绍TiDB SQL编写规范与优化方法,包括命名规范、SQL编写规范、索引设计规范、查询优化、DML优化、事务优化等内容。风哥教程参考TiDB官方文档SQL语法、性能调优指南等内容,适合开发人员和DBA编写高性能TiDB SQL。
Part01-基础概念与理论知识
1.1 TiDB SQL概述
TiDB支持标准SQL语法,与MySQL高度兼容。TiDB的SQL层负责解析、优化和执行SQL语句,支持分布式事务、在线DDL、窗口函数等高级特性。
- 高度兼容MySQL 5.7语法
- 支持分布式事务(ACID)
- 支持在线DDL操作
- 支持窗口函数和CTE
- 支持分区表
1.2 MySQL兼容性说明
TiDB与MySQL的兼容性:
- 完全兼容:标准SQL语法、基本数据类型、常用函数
- 部分兼容:存储过程、触发器、视图
- 不兼容:外键约束(忽略)、部分系统变量
1.3 SQL执行原理
TiDB SQL执行流程:解析 -> 预处理 -> 优化 -> 执行 -> 返回结果。优化器会生成执行计划,选择最优的访问路径。
Part02-生产环境规划与建议
2.1 命名规范
# 1. 数据库命名
– 使用小写字母和下划线
– 格式:业务_模块,如:fgedu_order、fgedu_user
– 长度不超过32个字符
– 避免使用保留字
# 2. 表命名
– 使用小写字母和下划线
– 格式:模块_表名,如:fgedu_orders、fgedu_users
– 表名使用复数形式
– 长度不超过64个字符
# 3. 字段命名
– 使用小写字母和下划线
– 主键:id 或 表名_id
– 外键:关联表名_id
– 时间字段:created_at、updated_at
– 状态字段:status、is_deleted
# 4. 索引命名
– 主键:PRIMARY
– 唯一索引:uk_字段名
– 普通索引:idx_字段名
– 组合索引:idx_字段1_字段2
2.2 SQL编写规范
# 1. 关键字使用大写
SELECT id, username, email
FROM fgedu_users
WHERE status = ‘active’
ORDER BY created_at DESC
LIMIT 10;
# 2. 避免使用SELECT *
# 错误
SELECT * FROM fgedu_orders;
# 正确
SELECT order_id, user_id, amount, status
FROM fgedu_orders;
# 3. 使用显式字段列表插入
# 错误
INSERT INTO fgedu_users VALUES (1, ‘fgedu01’, ‘fgedu01@fgedu.net.cn’);
# 正确
INSERT INTO fgedu_users (id, username, email)
VALUES (1, ‘fgedu01’, ‘fgedu01@fgedu.net.cn’);
# 4. 使用预编译语句
# 推荐
PREPARE stmt FROM ‘SELECT * FROM fgedu_users WHERE id = ?’;
SET @id = 1;
EXECUTE stmt USING @id;
# 5. 避免在WHERE条件中使用函数
# 错误
SELECT * FROM fgedu_users WHERE DATE(created_at) = ‘2024-04-09’;
# 正确
SELECT * FROM fgedu_users
WHERE created_at >= ‘2024-04-09 00:00:00’
AND created_at < '2024-04-10 00:00:00';
2.3 索引设计规范
# 1. 主键索引
– 每个表必须有主键
– 主键建议使用自增ID或雪花算法ID
– 避免使用业务字段作为主键
# 2. 唯一索引
– 用于唯一约束的字段
– 如:用户名、邮箱、手机号
# 3. 普通索引
– 频繁查询的字段
– 外键字段
– 排序字段
# 4. 组合索引
– 遵循最左前缀原则
– 区分度高的字段放在前面
– 避免过多字段组合
# 示例
CREATE TABLE fgedu_orders (
order_id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
order_no VARCHAR(32) NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status VARCHAR(20) DEFAULT ‘pending’,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_order_no (order_no),
KEY idx_user_id (user_id),
KEY idx_status_created (status, created_at)
);
风哥提示:
Part03-生产环境项目实施方案
3.1 查询优化实战
3.1.1 使用EXPLAIN分析执行计划
mysql> EXPLAIN SELECT * FROM fgedu_users WHERE id = 1;
+————————-+———-+———–+—————+——————————–+
| id | estRows | task | access object | operator info |
+————————-+———-+———–+—————+——————————–+
| Point_Get_1 | 1.00 | root | table:fgedu_users | handle:1 |
+————————-+———-+———–+—————+——————————–+
1 row in set (0.00 sec)
# 复杂查询执行计划
mysql> EXPLAIN SELECT u.username, o.order_no, o.amount
-> FROM fgedu_users u
-> JOIN fgedu_orders o ON u.id = o.user_id
-> WHERE o.status = ‘completed’
-> ORDER BY o.amount DESC
-> LIMIT 10;
+——————————+———-+———–+————————+——————————–+
| id | estRows | task | access object | operator info |
+——————————+———-+———–+————————+——————————–+
| TopN_12 | 10.00 | root | | fgedu_orders.amount:desc, offset:0, count:10 |
| └─HashJoin_17 | 10.00 | root | | inner join, equal:[eq(fgedu_users.id, fgedu_orders.user_id)] |
| ├─TableReader_23(Build) | 10000.00 | root | | data:Selection_22 |
| │ └─Selection_22 | 10000.00 | cop[tikv]| | eq(fgedu_orders.status, “completed”) |
| │ └─TableFullScan_21 | 100000.00| cop[tikv]| table:o | keep order:false, stats:pseudo |
| └─TableReader_20(Probe) | 10000.00 | root | | data:TableFullScan_19 |
| └─TableFullScan_19 | 10000.00 | cop[tikv]| table:u | keep order:false, stats:pseudo |
+——————————+———-+———–+————————+——————————–+
7 rows in set (0.01 sec)
3.1.2 优化查询语句
mysql> EXPLAIN SELECT * FROM fgedu_orders WHERE user_id = 10001;
+————————-+———-+———–+——————+——————————–+
| id | estRows | task | access object | operator info |
+————————-+———-+———–+——————+——————————–+
| TableReader_7 | 10000.00 | root | | data:Selection_6 |
| └─Selection_6 | 10000.00 | cop[tikv] | | eq(fgedu_orders.user_id, 10001)|
| └─TableFullScan_5 | 100000.00| cop[tikv] | table:fgedu_orders | keep order:false |
+————————-+———-+———–+——————+——————————–+
3 rows in set (0.00 sec)
# 添加索引
mysql> CREATE INDEX idx_user_id ON fgedu_orders(user_id);
Query OK, 0 rows affected (2.34 sec)
# 优化后:索引扫描
mysql> EXPLAIN SELECT * FROM fgedu_orders WHERE user_id = 10001;
+————————+———-+———–+——————+——————————–+
| id | estRows | task | access object | operator info |
+————————+———-+———–+——————+——————————–+
| IndexLookUp_10 | 10.00 | root | | |
| ├─IndexRangeScan_8(Build)| 10.00 | cop[tikv] | table:fgedu_orders, index:idx_user_id(user_id) | range:[10001,10001], keep order:false |
| └─TableRowIDScan_9(Probe)| 10.00 | cop[tikv] | table:fgedu_orders | keep order:false |
+————————+———-+———–+——————+——————————–+
3 rows in set (0.00 sec)
3.2 DML优化实战
3.2.1 批量插入优化
mysql> INSERT INTO fgedu_logs (user_id, action) VALUES (1, ‘login’);
Query OK, 1 row affected (0.05 sec)
mysql> INSERT INTO fgedu_logs (user_id, action) VALUES (2, ‘login’);
Query OK, 1 row affected (0.04 sec)
# 优化后:批量插入
mysql> INSERT INTO fgedu_logs (user_id, action) VALUES
-> (1, ‘login’), (2, ‘login’), (3, ‘login’), (4, ‘login’), (5, ‘login’);
Query OK, 5 rows affected (0.06 sec)
Records: 5 Duplicates: 0 Warnings: 0
# 更大批量(建议每批1000-5000条)
# 使用LOAD DATA批量导入
mysql> LOAD DATA LOCAL INFILE ‘/data/fgedu_logs.csv’
-> INTO TABLE fgedu_logs
-> FIELDS TERMINATED BY ‘,’
-> LINES TERMINATED BY ‘\n’
-> (user_id, action, created_at);
Query OK, 100000 rows affected (5.23 sec)
Records: 100000 Deleted: 0 Skipped: 0 Warnings: 0
3.2.2 批量更新优化
# 错误做法,性能差
# 优化后:使用CASE WHEN批量更新
mysql> UPDATE fgedu_products
-> SET price = CASE product_id
-> WHEN 1 THEN 45000.00
-> WHEN 2 THEN 2800.00
-> WHEN 3 THEN 14000.00
-> END
-> WHERE product_id IN (1, 2, 3);
Query OK, 3 rows affected (0.02 sec)
Rows matched: 3 Changed: 3 Warnings: 0
# 或者使用JOIN更新
mysql> UPDATE fgedu_products p
-> JOIN fgedu_price_updates pu ON p.product_id = pu.product_id
-> SET p.price = pu.new_price
-> WHERE pu.update_date = ‘2024-04-09’;学习交流加群风哥QQ113257174
Query OK, 50 rows affected (0.15 sec)
Rows matched: 50 Changed: 50 Warnings: 0
3.3 事务优化实战
3.3.1 事务大小控制
START TRANSACTION;
— 插入100万条数据
INSERT INTO fgedu_logs …;
COMMIT;
— 可能导致OOM或锁超时
# 优化后:分批提交
SET @batch_size = 1000;
SET @total = 1000000;
SET @offset = 0;
WHILE @offset < @total DO
START TRANSACTION;
INSERT INTO fgedu_logs (user_id, action, created_at)
SELECT user_id, 'batch_action', NOW()
FROM fgedu_users
LIMIT @offset, @batch_size;
COMMIT;
SET @offset = @offset + @batch_size;
END WHILE;
# 实际执行
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO fgedu_logs (user_id, action)
-> SELECT id, ‘batch_insert’ FROM fgedu_users LIMIT 1000;
Query OK, 1000 rows affected (0.05 sec)
Records: 1000 Duplicates: 0 Warnings: 0
mysql> COMMIT;
Query OK, 0 rows affected (0.02 sec)
Part04-生产案例与实战讲解
4.1 慢查询优化案例
4.1.1 慢查询分析与优化
mysql> SELECT * FROM information_schema.slow_query
-> WHERE query_time > 1
-> ORDER BY query_time DESC
-> LIMIT 5;
+—————-+——————+———————————-+
| query_time | query | digest |
+—————-+——————+———————————-+
| 12.5s | SELECT * FROM fgedu_orders WHERE user_id IN (SELECT user_id FROM fgedu_users WHERE status = ‘inactive’) | abc123… |
| 8.3s | SELECT COUNT(*) FROM fgedu_logs WHERE created_at > ‘2024-01-01’ | def456… |
+—————-+——————+———————————-+
2 rows in set (0.02 sec)
# 2. 分析执行计划
mysql> EXPLAIN SELECT * FROM fgedu_orders
-> WHERE user_id IN (SELECT user_id FROM fgedu_users WHERE status = ‘inactive’);
+————————————+———-+———–+——————+——————————–+
| id | estRows | task | access object | operator info |
+————————————+———-+———–+——————+——————————–+
| HashJoin_9 | 10000.00 | root | | inner join, equal:[eq(fgedu_orders.user_id, fgedu_users.user_id)] |
| ├─TableReader_14(Build) | 10000.00 | root | | data:Selection_13 |
| │ └─Selection_13 | 10000.00 | cop[tikv] | | eq(fgedu_users.status, “inactive”) |
| │ └─TableFullScan_12 | 100000.00| cop[tikv] | table:fgedu_users| keep order:false |
| └─TableReader_11(Probe) | 100000.00| root | | data:TableFullScan_10 |
| └─TableFullScan_10 | 100000.00| cop[tikv] | table:fgedu_orders| keep order:false |
+————————————+———-+———–+——————+——————————–+
6 rows in set (0.01 sec)
# 3. 优化:使用EXISTS替代IN,添加索引
mysql> CREATE INDEX idx_status ON fgedu_users(status);
Query OK, 0 rows affected (1.23 sec)
mysql> EXPLAIN SELECT o.* FROM fgedu_orders o
-> WHERE EXISTS (SELECT 1 FROM fgedu_users u WHERE u.user_id = o.user_id AND u.status = ‘inactive’);
+————————————+———-+———–+——————+——————————–+
| id | estRows | task | access object | operator info |
+————————————+———-+———–+——————+——————————–+
| IndexJoin_11 | 10000.00 | root | | inner join, inner:TableReader_10, outer key:fgedu_orders.user_id, inner key:fgedu_users.user_id, equal cond:eq(fgedu_orders.user_id, fgedu_users.user_id) |
| ├─TableReader_18(Build) | 100000.00| root | | data:TableFullScan_17 |
| │ └─TableFullScan_17 | 100000.00| cop[tikv] | table:fgedu_orders| keep order:false |
| └─TableReader_10(Probe) | 1.00 | root | | data:Selection_9 |
| └─Selection_9 | 1.00 | cop[tikv] | | eq(fgedu_users.status, “inactive”) |
| └─Point_Get_8 | 1.00 | cop[tikv] | table:fgedu_users| handle:10001 |
+————————————+———-+———–+——————+——————————–+
6 rows in set (0.00 sec)
# 4. 验证优化效果
# 优化前:12.5s
# 优化后:0.8s
# 性能提升:15倍
4.2 JOIN优化案例
4.2.1 JOIN顺序优化
mysql> EXPLAIN SELECT o.*, u.username, p.product_name
-> FROM fgedu_orders o
-> JOIN fgedu_users u ON o.user_id = u.id
-> JOIN fgedu_products p ON o.product_id = p.product_id
-> WHERE o.status = ‘completed’;
# 执行计划显示:orders表(100万)作为驱动表
# 优化后:调整JOIN顺序,使用STRAIGHT_JOIN
mysql> EXPLAIN SELECT o.*, u.username, p.product_name
-> FROM fgedu_users u
-> STRAIGHT_JOIN fgedu_orders o ON o.user_id = u.id
-> STRAIGHT_JOIN fgedu_products p ON o.product_id = p.product_id
-> WHERE o.status = ‘completed’ AND u.id < 1000;
# 执行计划显示:users表(1000条)作为驱动表
# 或者使用优化器Hint
mysql> EXPLAIN SELECT /*+ INL_JOIN(o, u) */ o.*, u.username
-> FROM fgedu_orders o
-> JOIN fgedu_users u ON o.user_id = u.id
-> WHERE o.status = ‘completed’;
# 强制使用Index Nested Loop Join
4.3 分页优化案例
4.3.1 深分页优化
mysql> SELECT * FROM fgedu_orders
-> ORDER BY created_at DESC
-> LIMIT 1000000, 10;
# 执行时间:5.2s
# 需要扫描1000010条记录
# 优化方案1:使用覆盖索引
mysql> CREATE INDEX idx_created_id ON fgedu_orders(created_at, order_id);
Query OK, 0 rows affected (3.45 sec)
mysql> SELECT o.* FROM fgedu_orders o
-> JOIN (
-> SELECT order_id FROM fgedu_orders
-> ORDER BY created_at DESC
-> LIMIT 1000000, 10
-> ) tmp ON o.order_id = tmp.order_id;
# 执行时间:0.8s
# 优化方案2:使用游标分页
# 上一页最后一条记录的时间戳:2024-04-01 10:00:00
mysql> SELECT * FROM fgedu_orders
-> WHERE created_at < '2024-04-01 10:00:00'
-> ORDER BY created_at DESC
-> LIMIT 10;
# 执行时间:0.1s
# 优化方案3:使用TiFlash加速(大数据量)
mysql> SELECT /*+ read_from_storage(tiflash[fgedu_orders]) */ *
-> FROM fgedu_orders
-> ORDER BY created_at DESC
-> LIMIT 1000000, 10;
# 执行时间:0.5s
Part05-风哥经验总结与分享
5.1 SQL优化最佳实践
- EXPLAIN是必备工具:每次优化前都要查看执行计划
- 索引是优化基础:合理的索引设计能解决80%的性能问题
- 避免全表扫描:大表查询必须有索引支持
- 控制事务大小:避免大事务,分批提交
- 使用预编译:减少SQL解析开销,防止SQL注入
5.2 常见反模式
# 问题:返回不必要的字段,增加网络开销
# 解决:明确指定需要的字段
# 反模式2:隐式类型转换
# 问题:导致索引失效
SELECT * FROM fgedu_orders WHERE user_id = ‘10001’; # user_id是BIGINT
# 反模式3:在WHERE中使用函数
# 问题:无法使用索引
SELECT * FROM fgedu_users WHERE YEAR(created_at) = 2024;
# 反模式4:OR条件优化不当
# 问题:可能导致索引失效
SELECT * FROM fgedu_orders WHERE user_id = 1 OR status = ‘completed’;
# 优化:使用UNION ALL
SELECT * FROM fgedu_orders WHERE user_id = 1
UNION ALL
SELECT * FROM fgedu_orders WHERE status = ‘completed’ AND user_id <> 1;
# 反模式5:NOT IN子查询
# 问题:性能差,可能返回错误结果
SELECT * FROM fgedu_users WHERE id NOT IN (SELECT user_id FROM fgedu_orders);
# 优化:使用NOT EXISTS或LEFT JOIN
SELECT * FROM fgedu_users u
WHERE NOT EXISTS (SELECT 1 FROM fgedu_orders o WHERE o.user_id = u.id);
5.3 优化工具推荐
EXPLAIN FORMAT=’verbose’ SELECT …;
EXPLAIN ANALYZE SELECT …; # 显示实际执行时间
# 2. Statement Summary
SELECT * FROM information_schema.statements_summary
WHERE avg_latency > 1000000000 # 查找慢查询
ORDER BY avg_latency DESC;
# 3. Slow Query Log
SET GLOBAL tidb_slow_log_threshold = 300; # 设置慢查询阈值300ms
SELECT * FROM information_schema.slow_query
WHERE query_time > 1 ORDER BY query_time DESC;
# 4. Dashboard分析
# TiDB Dashboard -> SQL Statements
# 查看SQL执行统计、慢查询、SQL诊断
# 5. 可视化工具
# – TiDB Dashboard
# – Grafana监控面板
# – TiDB Cloud Query Editor
本文由风哥教程整理发布,仅用于学习测试使用,转载注明出处:http://www.fgedu.net.cn/10327.html
