MySQL查询优化与EXPLAIN执行计划分析深度解析
深入探讨MySQL查询优化器工作原理、EXPLAIN执行计划详细解读和SQL性能调优实战,结合生产环境经验分享慢查询分析与优化策略。
🤔 问题背景与技术演进
我们要解决什么问题?
在现代Web应用中,数据库查询性能往往是系统瓶颈的关键因素。随着业务数据量的增长,原本毫秒级的简单查询可能变成秒级甚至分钟级的慢查询,严重影响用户体验。特别是在高并发场景下,一个慢查询可能导致数据库连接池耗尽,引发雪崩效应。
查询优化要解决的核心问题包括:减少数据扫描量、提高索引利用率、优化JOIN操作、减少临时表使用、避免文件排序等。这些问题如果不及时发现和解决,会导致数据库CPU飙升、内存不足、磁盘IO过载,最终影响整个系统的稳定性。
没有这个技术时是怎么做的?
在早期的数据库系统中,查询优化主要依靠开发人员的经验和直觉。开发者需要手动分析SQL语句,猜测数据库的执行路径,通过反复试验来优化查询性能。这种方式存在几个严重问题:
盲目优化:没有科学的分析工具,只能凭感觉修改SQL,可能越改越慢。调试困难:无法准确了解查询的执行过程,难以定位性能瓶颈。维护成本高:随着数据量变化,原本高效的查询可能变慢,需要重新优化。
那时的开发者常常使用一些原始方法:通过时间戳记录查询耗时、使用SHOW PROCESSLIST
查看正在执行的查询、通过慢查询日志事后分析问题。这些方法虽然能发现问题,但无法提供足够的细节信息来指导优化。
技术演进的历史脉络
MySQL查询优化技术的发展经历了几个重要阶段:
MySQL 3.x时代(1990s):基础的查询优化器,主要依靠简单的成本估算。MySQL 4.x时代(2000s):引入了EXPLAIN命令,开发者首次能够查看查询的执行计划。MySQL 5.x时代(2005-2010):查询优化器大幅改进,支持更复杂的JOIN优化和子查询优化。MySQL 5.6+时代(2013+):引入了EXPLAIN FORMAT=JSON,提供更详细的执行信息。MySQL 8.0时代(2018+):新增了EXPLAIN ANALYZE,能够显示实际的执行统计信息。
这个演进过程体现了数据库系统从”黑盒”向”白盒”的转变,让开发者能够深入了解查询的执行过程,进行精准的性能调优。
🎯 核心概念与原理
基础概念定义
**查询优化器(Query Optimizer)**是MySQL的核心组件,负责将SQL语句转换为最优的执行计划。它是一个基于成本的优化器(Cost-Based Optimizer,CBO),通过估算不同执行路径的代价来选择最优方案。
**执行计划(Execution Plan)**是查询优化器为SQL语句生成的具体执行步骤,包括表的访问顺序、索引的使用情况、JOIN算法的选择等。EXPLAIN命令就是用来展示这个执行计划的工具。
**查询成本(Query Cost)**是优化器评估执行计划优劣的标准,主要包括CPU成本和IO成本。MySQL通过统计信息和内置的成本模型来估算不同操作的代价。
工作原理详解
MySQL查询优化器的工作流程可以分为几个关键步骤:
1. 语法解析阶段:将SQL语句解析成抽象语法树(AST),检查语法错误和权限。
2. 预处理阶段:展开视图、子查询,进行语义检查,确定表和列的存在性。
3. 优化阶段:这是最核心的阶段,包括:
- 逻辑优化:常量折叠、谓词下推、列裁剪等
- 物理优化:选择访问路径、JOIN顺序、JOIN算法等
- 成本估算:计算不同执行计划的代价
4. 执行计划生成:选择成本最低的执行计划,生成可执行的操作序列。
查询优化器使用动态规划算法来搜索最优的JOIN顺序。对于N个表的JOIN,理论上有N!种可能的顺序,优化器通过剪枝和启发式算法来减少搜索空间。
技术特点和优势
MySQL查询优化器具有以下核心特点:
自适应性强:能够根据表的统计信息自动调整执行计划,适应数据分布的变化。成本准确性:通过精确的成本模型,能够比较准确地估算不同执行路径的代价。优化范围广:不仅优化单表查询,还能优化复杂的多表JOIN、子查询、聚合查询等。
相比传统的基于规则的优化器(Rule-Based Optimizer,RBO),基于成本的优化器能够更好地适应不同的数据特征和查询模式,生成更优的执行计划。
🔧 实现原理与源码分析
底层实现机制
MySQL查询优化器的核心实现位于sql/sql_optimizer.cc
文件中。优化器使用了多种算法和数据结构来实现高效的查询优化:
JOIN顺序优化使用动态规划算法,通过optimize_straight_join()
函数实现。对于少量表的JOIN,使用穷举法;对于大量表的JOIN,使用贪心算法和启发式搜索。
成本计算模型定义在sql/opt_costmodel.h
中,包括以下关键参数:
ROW_EVALUATE_COST
:评估一行数据的CPU成本KEY_COMPARE_COST
:索引比较的CPU成本DISK_SEEK_BASE_COST
:磁盘寻道的IO成本DISK_READ_COST
:读取数据页的IO成本
关键源码解读
以下是MySQL查询优化器的核心代码结构:
// sql/sql_optimizer.cc - 查询优化器主要逻辑
class JOIN {
private:
TABLE_LIST *tables; // 参与JOIN的表列表
uint table_count; // 表的数量
POSITION *best_positions; // 最优的表访问顺序
double best_read; // 最优执行计划的成本
public:
int optimize(); // 主优化函数
bool choose_plan(); // 选择执行计划
void set_access_path(); // 设置表访问路径
};
// 优化器主要流程
int JOIN::optimize() {
// 1. 预处理WHERE条件
if (optimize_cond(this, &where_cond, &cond_equal))
return 1;
// 2. 选择最优的表访问顺序
if (choose_plan())
return 1;
// 3. 为每个表选择最优的访问路径
for (uint i = 0; i < table_count; i++) {
set_access_path_for_table(i);
}
return 0;
}
// 成本计算的核心函数
double calculate_scan_cost(TABLE *table, uint key_no,
double rows_to_read) {
Cost_model_table *cost_model = table->cost_model();
if (key_no == MAX_KEY) {
// 全表扫描成本
return cost_model->page_read_cost(table->file->stats.data_file_length);
} else {
// 索引扫描成本
KEY *key_info = table->key_info + key_no;
double index_cost = cost_model->key_read_cost(key_info, rows_to_read);
return index_cost;
}
}
设计思想分析
MySQL查询优化器的设计体现了几个重要的工程原则:
分层设计:将优化过程分为逻辑优化和物理优化两层,逻辑优化关注SQL语义的等价变换,物理优化关注具体的执行方式选择。
成本驱动:所有的优化决策都基于成本估算,通过量化的方式比较不同方案的优劣。这种设计使得优化器能够适应不同的硬件环境和数据特征。
启发式搜索:对于复杂查询,使用启发式算法来减少搜索空间,在优化质量和优化时间之间找到平衡。
统计信息依赖:优化器的决策严重依赖表和索引的统计信息,这要求定期更新统计信息以保证优化效果。
💡 实战案例与代码示例
具体项目应用
在一个电商项目中,我们遇到了订单查询性能问题。用户查询历史订单时,响应时间经常超过3秒,严重影响用户体验。通过EXPLAIN分析发现了多个性能瓶颈。
业务场景:用户查询指定时间范围内的订单,需要关联商品表显示商品信息,并按订单时间倒序排列。
原始SQL语句:
SELECT o.order_id, o.order_time, o.total_amount,
p.product_name, p.product_price
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id
WHERE o.user_id = 12345
AND o.order_time BETWEEN '2024-01-01' AND '2024-12-31'
ORDER BY o.order_time DESC
LIMIT 20;
完整代码实现
步骤1:使用EXPLAIN分析原始查询
-- 分析原始查询的执行计划
EXPLAIN FORMAT=JSON
SELECT o.order_id, o.order_time, o.total_amount,
p.product_name, p.product_price
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id
WHERE o.user_id = 12345
AND o.order_time BETWEEN '2024-01-01' AND '2024-12-31'
ORDER BY o.order_time DESC
LIMIT 20;
-- 输出结果分析
{
"query_block": {
"select_id": 1,
"cost_info": {
"query_cost": "2547.89" -- 查询成本很高
},
"ordering_operation": {
"using_filesort": true, -- 使用了文件排序
"cost_info": {
"sort_cost": "1247.89"
},
"nested_loop": [
{
"table": {
"table_name": "o",
"access_type": "range", -- 范围扫描
"key": "idx_user_id",
"rows_examined_per_scan": 1234, -- 扫描行数较多
"filtered": "11.11"
}
}
]
}
}
}
步骤2:创建复合索引优化查询
-- 创建覆盖索引优化ORDER BY和WHERE条件
CREATE INDEX idx_user_time_order ON orders(user_id, order_time DESC, order_id);
-- 为order_items表创建优化索引
CREATE INDEX idx_order_product ON order_items(order_id, product_id);
-- 重新分析优化后的执行计划
EXPLAIN FORMAT=JSON
SELECT o.order_id, o.order_time, o.total_amount,
p.product_name, p.product_price
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id
WHERE o.user_id = 12345
AND o.order_time BETWEEN '2024-01-01' AND '2024-12-31'
ORDER BY o.order_time DESC
LIMIT 20;
步骤3:进一步优化 - 使用子查询减少JOIN
-- 使用子查询先获取订单ID,减少JOIN的数据量
SELECT o.order_id, o.order_time, o.total_amount,
p.product_name, p.product_price
FROM (
SELECT order_id, order_time, total_amount
FROM orders
WHERE user_id = 12345
AND order_time BETWEEN '2024-01-01' AND '2024-12-31'
ORDER BY order_time DESC
LIMIT 20
) o
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id
ORDER BY o.order_time DESC;
-- 分析优化效果
EXPLAIN FORMAT=JSON [上述SQL];
最佳实践总结
慢查询监控配置:
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- 超过1秒的查询记录到慢查询日志
SET GLOBAL log_queries_not_using_indexes = 'ON';
-- 查看慢查询统计
SHOW GLOBAL STATUS LIKE 'Slow_queries';
-- 使用pt-query-digest分析慢查询日志
-- pt-query-digest /var/log/mysql/slow.log
索引优化策略:
-- 检查索引使用情况
SELECT
table_schema,
table_name,
index_name,
cardinality,
sub_part,
packed,
nullable,
index_type
FROM information_schema.statistics
WHERE table_schema = 'your_database'
ORDER BY table_name, seq_in_index;
-- 检查未使用的索引
SELECT
s.table_schema,
s.table_name,
s.index_name,
s.cardinality
FROM information_schema.statistics s
LEFT JOIN performance_schema.table_io_waits_summary_by_index_usage p
ON s.table_schema = p.object_schema
AND s.table_name = p.object_name
AND s.index_name = p.index_name
WHERE p.index_name IS NULL
AND s.table_schema NOT IN ('mysql', 'performance_schema', 'information_schema')
AND s.index_name != 'PRIMARY';
🎯 面试高频问题精讲
1. EXPLAIN命令的各个字段含义是什么?
标准答案:EXPLAIN命令显示MySQL如何执行SQL语句,主要字段包括:
- id:查询序列号,表示查询中执行select子句或操作表的顺序
- select_type:查询类型,如SIMPLE、PRIMARY、SUBQUERY、DERIVED等
- table:输出行所引用的表名
- partitions:匹配的分区信息
- type:表示表的连接类型,性能从好到差:system > const > eq_ref > ref > range > index > ALL
- possible_keys:查询可能使用的索引
- key:实际使用的索引
- key_len:使用索引的长度
- ref:显示索引的哪一列被使用了
- rows:MySQL认为必须检查的用来返回请求数据的行数
- filtered:按表条件过滤的行百分比
- Extra:包含MySQL解决查询的详细信息
扩展要点:type字段是最重要的性能指标。const表示通过主键或唯一索引访问,性能最好;ref表示通过非唯一索引访问;range表示范围扫描;index表示索引全扫描;ALL表示全表扫描,性能最差。
2. 什么情况下会出现Using filesort,如何优化?
标准答案:Using filesort出现在ORDER BY无法使用索引排序时,MySQL需要创建临时文件进行排序,严重影响性能。
常见原因包括:
- ORDER BY的列没有索引
- ORDER BY使用了表达式或函数
- 多表JOIN时排序列来自不同表
- 排序方向与索引方向不一致
优化策略:
-- 1. 为ORDER BY列创建索引
CREATE INDEX idx_order_time ON orders(order_time);
-- 2. 使用复合索引覆盖WHERE和ORDER BY
CREATE INDEX idx_user_time ON orders(user_id, order_time DESC);
-- 3. 调整sort_buffer_size参数
SET SESSION sort_buffer_size = 2097152; -- 2MB
面试技巧:强调filesort分为两种:一种是在内存中排序(快速),另一种是在磁盘上排序(慢速)。可以通过增加sort_buffer_size来提高内存排序的概率。
3. MySQL查询优化器是如何选择索引的?
标准答案:MySQL查询优化器基于成本模型选择索引,主要考虑因素包括:
统计信息:表行数、索引基数、数据分布等 选择性:索引能过滤掉多少数据,选择性越高越好 覆盖性:索引是否包含查询所需的所有列 排序需求:索引顺序是否匹配ORDER BY要求
成本计算公式:
总成本 = IO成本 + CPU成本
IO成本 = 需要读取的页数 × 页读取成本
CPU成本 = 需要处理的行数 × 行处理成本
扩展要点:优化器会比较全表扫描和各种索引扫描的成本,选择成本最低的方案。当表数据量很小时,优化器可能选择全表扫描而不是索引扫描。
4. 如何分析和优化JOIN查询的性能?
标准答案:JOIN查询优化需要从多个角度分析:
JOIN算法选择:
- Nested Loop Join:适用于小表驱动大表
- Hash Join:适用于等值连接且内存充足
- Sort Merge Join:适用于非等值连接
驱动表选择:选择结果集较小的表作为驱动表 索引优化:为JOIN条件和WHERE条件创建合适的索引 数据类型匹配:确保JOIN列的数据类型完全一致
优化示例:
-- 优化前:大表驱动小表
SELECT * FROM orders o
JOIN users u ON o.user_id = u.user_id
WHERE u.status = 'active';
-- 优化后:小表驱动大表
SELECT * FROM users u
JOIN orders o ON u.user_id = o.user_id
WHERE u.status = 'active';
-- 创建合适的索引
CREATE INDEX idx_user_status ON users(status, user_id);
CREATE INDEX idx_order_user ON orders(user_id);
5. 什么是索引下推(Index Condition Pushdown),有什么作用?
标准答案:索引下推是MySQL 5.6引入的优化技术,允许在索引遍历过程中直接过滤数据,减少回表次数。
工作原理:
- 传统方式:先通过索引找到主键,再回表获取完整行数据,最后应用WHERE条件
- 索引下推:在索引层面就应用部分WHERE条件,只有满足条件的记录才回表
适用场景:
-- 复合索引:idx_name_age(name, age)
SELECT * FROM users
WHERE name LIKE 'John%' AND age > 25;
-- 没有索引下推:通过name找到所有John开头的记录,全部回表,再过滤age
-- 有索引下推:在索引中就过滤age > 25,减少回表次数
EXPLAIN中的体现:Extra列显示”Using index condition”
6. 如何处理MySQL中的慢查询?
标准答案:慢查询处理是一个系统性的过程:
1. 监控发现:
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
-- 实时监控
SHOW FULL PROCESSLIST;
2. 分析定位:
-- 使用EXPLAIN分析执行计划
EXPLAIN FORMAT=JSON SELECT ...;
-- 使用SHOW PROFILE分析详细耗时
SET profiling = 1;
SELECT ...;
SHOW PROFILES;
SHOW PROFILE FOR QUERY 1;
3. 优化策略:
- 索引优化:创建合适的索引
- SQL重写:优化查询逻辑
- 表结构调整:分区、分表等
- 参数调优:调整MySQL配置参数
4. 效果验证:对比优化前后的执行计划和响应时间
7. EXPLAIN中的rows和filtered字段如何理解?
标准答案:
- rows:MySQL估计需要检查的行数,是优化器基于统计信息的估算值
- filtered:表示按WHERE条件过滤后剩余的行数百分比
计算关系:
实际处理的行数 = rows × filtered / 100
优化意义:
- rows值越小越好,说明扫描的数据量少
- filtered值越大越好,说明WHERE条件的过滤效果好
- 两者的乘积代表传递给下一个操作的行数
示例分析:
-- rows=1000, filtered=10.00
-- 表示扫描1000行,其中只有10%(100行)满足WHERE条件
-- 这种情况说明WHERE条件的选择性较差,可能需要优化索引
8. 如何理解MySQL的成本模型?
标准答案:MySQL的成本模型用于估算不同执行计划的代价,帮助优化器选择最优方案。
成本组成:
- IO成本:读取数据页的磁盘IO代价
- CPU成本:处理数据行的CPU计算代价
关键参数(可通过mysql.server_cost和mysql.engine_cost表查看):
-- 查看成本参数
SELECT * FROM mysql.server_cost;
SELECT * FROM mysql.engine_cost;
-- 主要参数含义
-- disk_temptable_create_cost: 创建磁盘临时表成本
-- disk_temptable_row_cost: 磁盘临时表行处理成本
-- key_compare_cost: 索引比较成本
-- memory_temptable_create_cost: 内存临时表创建成本
-- row_evaluate_cost: 行评估成本
实际应用:理解成本模型有助于理解为什么优化器在某些情况下选择全表扫描而不是索引扫描,特别是当表数据量很小时。
⚡ 性能优化与注意事项
性能瓶颈分析
常见性能瓶颈识别:
- 全表扫描问题:EXPLAIN显示type为ALL,rows值很大
- 索引失效问题:possible_keys有值但key为NULL
- 文件排序问题:Extra显示Using filesort
- 临时表问题:Extra显示Using temporary
- JOIN算法问题:多表JOIN时选择了低效的算法
瓶颈检测方法:
-- 检查正在执行的慢查询
SELECT
id, user, host, db, command, time, state, info
FROM information_schema.processlist
WHERE time > 10 AND command != 'Sleep'
ORDER BY time DESC;
-- 检查表的统计信息是否过期
SELECT
table_schema, table_name,
update_time, check_time,
table_rows, avg_row_length
FROM information_schema.tables
WHERE table_schema = 'your_database'
ORDER BY update_time;
-- 使用Performance Schema监控查询性能
SELECT
digest_text,
count_star,
avg_timer_wait/1000000000 as avg_time_seconds,
sum_rows_examined,
sum_rows_sent
FROM performance_schema.events_statements_summary_by_digest
ORDER BY avg_timer_wait DESC
LIMIT 10;
优化策略方案
系统级优化策略:
-- 1. 优化器相关参数
SET GLOBAL optimizer_search_depth = 62; -- 控制JOIN搜索深度
SET GLOBAL optimizer_prune_level = 1; -- 启用优化器剪枝
SET GLOBAL optimizer_switch = 'index_merge=on,index_merge_union=on';
-- 2. 缓冲区优化
SET GLOBAL innodb_buffer_pool_size = 8G; -- 设置为内存的70-80%
SET GLOBAL query_cache_size = 256M; -- 查询缓存大小
SET GLOBAL sort_buffer_size = 2M; -- 排序缓冲区
SET GLOBAL join_buffer_size = 2M; -- JOIN缓冲区
-- 3. 统计信息更新策略
SET GLOBAL innodb_stats_auto_recalc = ON;
SET GLOBAL innodb_stats_persistent = ON;
SET GLOBAL innodb_stats_sample_pages = 20;
查询级优化技巧:
-- 1. 使用FORCE INDEX强制使用特定索引
SELECT * FROM orders FORCE INDEX(idx_user_time)
WHERE user_id = 12345 ORDER BY order_time DESC;
-- 2. 使用SQL_NO_CACHE避免查询缓存干扰测试
SELECT SQL_NO_CACHE * FROM orders WHERE user_id = 12345;
-- 3. 使用STRAIGHT_JOIN控制JOIN顺序
SELECT STRAIGHT_JOIN * FROM users u
JOIN orders o ON u.user_id = o.user_id;
-- 4. 优化子查询为JOIN
-- 优化前
SELECT * FROM orders WHERE user_id IN (
SELECT user_id FROM users WHERE status = 'active'
);
-- 优化后
SELECT o.* FROM orders o
JOIN users u ON o.user_id = u.user_id
WHERE u.status = 'active';
常见坑点规避
统计信息过期陷阱:
-- 定期更新表统计信息
ANALYZE TABLE orders, users, products;
-- 检查统计信息的准确性
SELECT
table_name, table_rows,
(SELECT COUNT(*) FROM orders) as actual_rows
FROM information_schema.tables
WHERE table_name = 'orders';
索引选择性陷阱:
-- 检查索引的选择性
SELECT
COUNT(DISTINCT user_id) / COUNT(*) as selectivity
FROM orders;
-- 选择性低于0.1的索引通常效果不佳
-- 考虑创建复合索引或删除低选择性索引
数据类型不匹配陷阱:
-- 错误:字符串和数字比较会导致索引失效
SELECT * FROM orders WHERE user_id = '12345'; -- user_id是INT类型
-- 正确:保持数据类型一致
SELECT * FROM orders WHERE user_id = 12345;
函数使用陷阱:
-- 错误:在WHERE条件中使用函数会导致索引失效
SELECT * FROM orders WHERE DATE(order_time) = '2024-12-28';
-- 正确:使用范围查询
SELECT * FROM orders
WHERE order_time >= '2024-12-28 00:00:00'
AND order_time < '2024-12-29 00:00:00';
📚 总结与技术对比
核心要点回顾
MySQL查询优化是一个系统性的工程,需要掌握以下核心要点:
EXPLAIN分析能力:能够准确解读执行计划,识别性能瓶颈。索引设计原则:理解复合索引的最左前缀原则,合理设计覆盖索引。优化器工作原理:了解基于成本的优化机制,理解统计信息的重要性。性能监控体系:建立完整的慢查询监控和分析流程。
查询优化不是一次性的工作,而是需要持续关注和调整的过程。随着数据量的增长和业务模式的变化,原本高效的查询可能变成性能瓶颈,需要定期审查和优化。
与相关技术对比
特性 | MySQL EXPLAIN | PostgreSQL EXPLAIN | Oracle执行计划 | SQL Server执行计划 |
---|---|---|---|---|
成本显示 | 支持(JSON格式) | 支持(ANALYZE选项) | 详细成本信息 | 详细成本信息 |
实际统计 | 8.0+支持ANALYZE | 支持ANALYZE | 支持实际统计 | 支持实际统计 |
可视化工具 | MySQL Workbench | pgAdmin | Oracle SQL Developer | SSMS |
优化器提示 | 支持INDEX提示 | 支持丰富的提示 | 丰富的优化器提示 | 支持查询提示 |
统计信息 | 自动收集 | 自动收集 | 手动/自动收集 | 自动收集 |
MySQL的优势:
- EXPLAIN输出简洁易懂,学习成本低
- JSON格式提供详细的成本信息
- 优化器相对简单,行为可预测
MySQL的不足:
- 优化器功能相对简单,缺少高级优化技术
- 统计信息精度有限,影响成本估算准确性
- 缺少执行计划缓存机制
持续学习建议
深入学习方向:
- 源码研究:深入研究MySQL优化器源码,理解算法实现细节
- 性能调优:学习更多的性能调优技巧和最佳实践
- 监控工具:掌握pt-toolkit、PMM等专业监控工具
- 新版本特性:关注MySQL 8.0+的新优化器特性
学习资源推荐:
- 《高性能MySQL》- 经典的MySQL性能优化指南
- MySQL官方文档的优化器章节
- Percona博客的性能优化文章
- MySQL源码分析相关的技术博客
实践建议: 在生产环境中建立完整的查询性能监控体系,定期分析慢查询日志,持续优化数据库性能。同时要关注MySQL新版本的特性,适时升级以获得更好的优化器性能。
记住,查询优化是一门艺术,需要理论知识和实践经验的完美结合。通过不断的学习和实践,才能成为真正的MySQL性能调优专家。