ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于DeepSeek Harness的MySQL慢查询分析插件实战

基于DeepSeek Harness的MySQL慢查询分析插件实战 搞数据库的人大概都有这种体验线上反馈一条SQL很慢你把它拿出来EXPLAIN一下看到type是 ALL、rows好几万、Extra里还有 Using filesort然后得逐字段去对照表结构和索引心里盘算要不要加联合索引、要不要改写关联顺序。这套流程熟练工做起来也要几分钟更别说SQL复杂到涉及七八张表时光读懂执行计划就得费不少劲。我最近在用 DeepSeek Harness 做 AI 应用编排顺手写了一个数据库查询分析插件——输入一条 SQL插件自动采集 MySQL 的执行计划、表结构、索引信息再交给大模型生成一份带优化建议的诊断报告。实测下来对 MySQL 官方 sakila 示例库里的典型慢查询判断基本靠谱整个排查过程也从人肉读执行计划变成了人审 AI 结论。这篇文章把完整实现过程拆开讲一遍内容包括插件机制分析、环境准备、核心代码以及我在实测中踩过的四个坑。1. 为什么选 sakila 库做插件实战——数据库分析场景的天然试验田1.1 sakila 的业务模型与表结构优势sakila 是 MySQL 官方提供的示例数据库模拟的是一家 DVD 租赁店的完整业务。很多初学者不熟悉它但它的表设计其实比 world、employees 那两个示例库更适合做查询分析演示。world 只有 country、city、language 三张表结构太简单连一次像样的多表 JOIN 都凑不出来。employees 表结构偏扁平业务含义单一。而 sakila 里有 actor、film、category、customer、store、rental、payment、inventory 这些表彼此之间有明确的外键关系客户去门店租影片库存关联影片和门店支付记录关联客户和租赁单。整个库包含至少 23 张表、7 个视图、若干个存储过程和触发器字段类型覆盖了 INT、DECIMAL、VARCHAR、TEXT、DATETIME、BLOB索引结构也模拟了真实 OLTP 系统的常用设计。这个业务模型非常贴近真实生产环境。你在实际工作中遇到的慢查询很多就是从一张订单表关联几张维表按条件过滤再排序这种结构来的。sakila 把这类场景完整地复现了出来用它可以很自然地构造出多表 JOIN 没走索引、分组排序临时表过大、子查询优化不当等经典问题。1.2 数据量不大不小慢查询现象真实可复现做查询分析插件最怕的是测试数据量太少一条 SQL 怎么跑都快分析结果没有说服力。sakila 的数据量设计得很巧妙表名记录数说明rental16044租赁记录业务核心表payment16049支付记录与 rental 一一对应inventory4581库存条目同一影片有多个副本film1000影片主数据actor200演员主数据customer599客户主数据rental 表 1.6 万条记录放在开发环境里单表查询毫秒级返回但一旦 JOIN 方式不合理rows估算值会瞬间放大到几万甚至十几万执行计划的差异非常明显。比如查询每个客户最近一次租赁记录这种需求用相关子查询写和用窗口函数写执行计划完全不同插件完全能捕捉到这种差异。数据量小到测试迭代快大到慢查询特征真实可见这就是我选它做插件实战的原因。2. 先弄懂 DeepSeek Harness 的插件机制它凭什么能挂载分析能力2.1 Harness 在整个链路里的位置先说清楚 DeepSeek Harness 到底干了什么。它本质上是一个面向大模型应用的编排框架你可以把它理解成大模型的操作系统模型是 CPU负责决策插件是应用程序负责具体执行Harness 本身管的是进程调度、资源分配、上下文管理这些底层逻辑。没有 Harness 的时候你要让大模型分析 SQL只能把 SQL 和 EXPLAIN 结果手动拼进提示词里发给模型然后祈祷它给出靠谱的回答。一旦需要动态查表结构、动态执行 EXPLAIN、多次调用工具手动拼提示词的模式就崩了——上下文越长越乱格式稍有不一致模型就理解偏了。Harness 解决的核心问题是让模型在需要某个能力时通过标准接口去调用外部工具工具执行的返回值再自动回到模型上下文里。模型可以边看边查、查完再答。我这次写的查询分析插件就是挂在 Harness 上的一个能力单元。2.2 插件的核心抽象工具注册、参数 Schema、上下文DeepSeek Harness 的插件机制核心围绕三个抽象概念展开。工具注册Tool Registration。插件向 Harness 声明自己提供哪些可调用函数每个函数要有名字、描述、参数Schema。描述很重要模型靠描述判断什么时候该调用这个工具参数Schema则规定了模型必须传什么参数。描述写得不清楚模型就可能该调不调、不该调乱调。上下文注入Context Injection。插件在执行前可以读取当前会话的上下文比如用户输入、历史消息、会话级变量。执行后的结果也会写回上下文供模型后续分析。生命周期钩子Lifecycle Hooks。Harness 允许插件在工具执行前“工具执行后”会话开始“会话结束”这些节点插入自定义逻辑。比如我可以在执行 EXPLAIN 前检查 SQL 类型非 SELECT 直接拒绝在执行后把原始结果压缩一下再放回上下文。2.3 查询分析为什么天然适合插件化查询分析不是单次操作而是一个链条拿到 SQL - 解析语义 - 采集执行计划 - 获取表结构 - 获取索引信息 - 生成诊断 - 给出改写建议。这个链条有两个特点让它天然适合插件化。一是每个环节都需要和外部系统交互。执行计划在 MySQL 里表结构在 information_schema 里索引信息要通过 SHOW INDEX 拿。这些都属于模型自己做不到、必须借助工具的事。二是环节之间存在依赖关系。如果采集到的执行计划里显示某张表 typeALL模型大概率还想看看这张表的索引情况这时就需要再次调用工具。Harness 的插件机制允许工具之间通过上下文传递中间结果模型可以动态决定下一步调什么。用插件化的方式把这个链条封装好之后模型那边的工作就变得很纯粹先调采集工具拿数据再基于数据推理分析。这也是我在标题里强调插件而不是一个 Python 脚本的原因——脚本是一次性的插件是可以在 Harness 里被模型自主调度、反复使用的能力原子。3. 环境准备MySQL 8.0、sakila 数据导入与最小权限账号3.1 MySQL 8.0 安装里容易被忽略的三个配置MySQL 安装本身不复杂但从能用到适合跑查询分析插件有三个细节容易被忽略。第一字符集一定要选 utf8mb4。MySQL 8.0 默认字符集已经是 utf8mb4但部分老版本或手动初始化的实例仍然是 latin1。sakila 数据里没有中文但插件未来要分析真实业务库字符集不对会导致乱码和索引长度问题。安装后可以用SHOW VARIABLES LIKE character_set_server;确认。第二认证插件要留意。MySQL 8.0 默认用 caching_sha2_passwordPython 的 pymysql 新版支持但如果用老版本驱动连接时会报Authentication plugin caching_sha2_password cannot be loaded。建议直接用最新版 pymysql或者给插件专用账号指定 mysql_native_password。第三要确保 EXPLAIN ANALYZE 能跑。MySQL 8.0.18 之后支持EXPLAIN ANALYZE它会真正执行SQL并返回实际耗时和行数。这个功能对插件价值很大但它依赖于 performance_schema 的开启。默认是开启的但有些云RDS或精简安装会关掉它可以执行SHOW VARIABLES LIKE performance_schema;检查OFF 的话需要在配置文件的 mysqld 段加performance_schemaON。3.2 导入 sakila 的完整命令与验证sakila 脚本可以从 MySQL 官方示例库仓库获取包含两个文件sakila-schema.sql建表和 sakila-data.sql数据。导入命令如下mysql -uroot -p sakila-schema.sql mysql -uroot -p sakila-data.sql注意导入顺序必须先 schema 后 data否则外键约束会报错。导入完成后验证一下USE sakila; SHOW TABLES; SELECT COUNT(*) FROM rental; SELECT COUNT(*) FROM payment;如果 rental 返回 16044、payment 返回 16049说明数据完整。另外可以看下存储过程是否也进来了SHOW PROCEDURE STATUS WHERE Db sakila;sakila 带了一些存储过程比如 film_in_stock、inventory_in_stock后面测试插件时可以用来验证插件对调用存储过程这类场景的处理。3.3 给插件专用的只读数据库账号查询分析插件要连数据库但绝不能拿 root 账号给插件用。安全原则是最小权限插件只需要读取数据字典和执行 EXPLAIN给个只读账号就够。CREATE USER query_analyzerlocalhost IDENTIFIED BY your_strong_password; GRANT SELECT ON sakila.* TO query_analyzerlocalhost; GRANT SHOW VIEW ON sakila.* TO query_analyzerlocalhost;SHOW VIEW 权限是为了让插件能读取视图定义。如果插件后续要分析其他库按需授权即可。不建议给 INSERT、UPDATE、DELETE 权限EXPLAIN 本身不需要写权限。这样即使插件被注入恶意 SQL 文本影响范围也控制在只读层面。4. 查询分析插件核心实现从 EXPLAIN 采集到诊断报告4.1 插件的三层结构设计我把插件分成了三层这样职责清晰也方便单独扩展。采集层负责和 MySQL 交互执行 EXPLAIN、SHOW CREATE TABLE、SHOW INDEX FROM把结果变成结构化数据。分析层负责从执行计划里提取关键特征比如全表扫描、索引失效、排序临时表整理成模型容易理解的摘要。建议层则是模型自己的工作——插件把采集和分析结果给模型模型基于这些信息生成优化建议。这个分层的核心价值在于采集层保证数据真实分析层保证数据可理解建议层保证回答有针对性。如果跳过分析层直接把原始 EXPLAIN JSON 丢给模型后面第五章会讲效果会很差。4.2 工具注册与参数定义在 DeepSeek Harness 里注册工具需要按规范提供 Schema。下面是我插件里核心工具的注册定义TOOLS [ { type: function, function: { name: analyze_sql_plan, description: 分析一条SELECT查询在MySQL中的执行计划返回执行计划摘要、表结构信息、索引信息。支持sakila示例库。, parameters: { type: object, properties: { sql_text: { type: string, description: 需要分析的SQL语句必须是SELECT查询 }, database: { type: string, description: 目标数据库名称默认sakila } }, required: [sql_text] } } } ]description 里我特意写了必须是SELECT查询这是给模型看的约束。模型如果拿到一条 DELETE 语句大概率会因为描述里的限制转而询问用户或者直接拒绝分析。4.3 EXPLAIN FORMATJSON 采集函数采集函数是插件的核心。我用了EXPLAIN FORMATJSON而不是默认的表格格式原因后面再说。核心代码如下import pymysql import json from dbutils.pooled_db import PooledDB POOL PooledDB( creatorpymysql, host127.0.0.1, userquery_analyzer, passwordyour_strong_password, databasesakila, charsetutf8mb4, maxconnections5, blockingTrue, read_timeout5 ) def fetch_explain(sql_text: str, db: str sakila) - dict: conn POOL.connection() try: with conn.cursor() as cur: cur.execute(fUSE {db}) cur.execute(fEXPLAIN FORMATJSON {sql_text}) row cur.fetchone() return json.loads(row[0]) finally: conn.close()注意两点。一是用USE \db切换数据库而不是在连接串里固定库名这样插件能灵活处理不同库。二是cur.execute(fEXPLAIN FORMATJSON {sql_text}) 这里存在 SQL 注入风险但因为我们只给了只读账号EXPLAIN 语句本身也不能改数据风险可控。如果插件要对外开放必须加 SQL 白名单校验或参数化手段。4.4 表结构与索引信息的上下文构建EXPLAIN 输出里只有表的别名没有表结构。模型要知道这张表有哪些索引、哪些字段才能判断缺什么索引。所以插件还要采集相关表的元数据def fetch_table_metadata(table_name: str, db: str sakila) - str: conn POOL.connection() try: with conn.cursor() as cur: cur.execute(fSHOW CREATE TABLE {db}.{table_name}) row cur.fetchone() ddl row[1] cur.execute(fSHOW INDEX FROM {db}.{table_name}) indexes [] for idx in cur.fetchall(): indexes.append({ key_name: idx[2], seq_in_index: idx[3], column_name: idx[4], non_unique: idx[1], index_type: idx[10] }) return {ddl: ddl, indexes: indexes} finally: conn.close()SHOW CREATE TABLE 返回的 DDL 信息很全但对模型来说太啰嗦。后面第五章我会讲怎么把它压成模型友好的紧凑格式。4.5 把采集结果送回模型生成报告采集完成后插件把三块内容拼成一个结构作为工具执行结果返回给 Harness[执行计划摘要] query_block: SELECT 语句块 table: rental, access_type: ALL, rows: 16044, filtered: 100, extra: Using temporary; Using filesort [索引信息] rental: PK(rental_id), idx_fk_inventory_id(inventory_id), idx_fk_customer_id(customer_id), idx_fk_staff_id(staff_id), idx_rental_date(rental_date) [表结构提示] rental 表记录租赁信息customer_id 关联 customer 表inventory_id 关联 inventory 表rental_date 记录租赁时间有单独索引。模型拿到这些信息后就能基于执行计划的特征给出诊断结论。我在实际使用中会让插件额外返回一句话以上是工具自动采集的真实执行计划数据rows 为估算值可能与实际行数有偏差这句话能明显降低模型把估算值当精确值的概率。5. 上下文工程怎么让大模型把执行计划看明白5.1 直接把 EXPLAIN JSON 丢给模型的三个问题我一开始偷懒把 EXPLAIN FORMATJSON 的原始返回直接拼进上下文给模型结果效果很差。总结下来有三个问题。一是冗余字段太多。一个最简单的单表查询EXPLAIN JSON 都有几十行包含 cost_info 里一堆嵌套数字prefix_cost、data_read_per_join 之类。模型不是人它分不清哪些是关键哪些是噪音注意力被稀释后重点反而抓不住。二是过长的数字串会干扰模型的数值判断。cost_info 里有类似prefix_cost: 15640.15这种精确到小数点后两位的数字模型很容易把这些当精确成本去比较而实际上优化器给的 cost 本来就是估计值。三是 JSON 的嵌套层级太深。query_block 下面嵌套 tabletable 下面嵌套 possible_keys、key、rows、filtered还有 attached_condition。模型的 transformer 结构对超长嵌套文本的处理能力有限深层级的 JSON 尤其容易导致信息丢失。5.2 特征提取规则与风险标记所以我加了一个特征提取层把 EXPLAIN JSON 抽成扁平的关键特征。提取规则如下def extract_features(explain_json: dict, sql_text: str) - list: features [] def walk(node, path): if table in node: table_name node[table][table_name] access_type node[table].get(access_type, ) rows node[table].get(rows, [0])[0] filtered node[table].get(filtered, [100])[0] extra node[table].get(attached_condition, []) item { table: table_name, access_type: access_type, rows_estimate: rows, filtered_pct: filtered, used_key: node[table].get(key, None), possible_keys: node[table].get(possible_keys, []) } features.append(item) for nested in node.get(nested_loop, []): walk(nested, path [table_name]) walk(explain_json.get(query_block, {}), []) return features提取之后我还做了一层风险标记规则很简单access_type 为 ALL标记全表扫描access_type 为 index标记索引全扫描可能退化为全表扫描key 为 NULL 且 possible_keys 不为空标记存在可用索引但未使用Extra 包含 filesort 或 temporary标记排序/分组使用临时文件rows 估算值超过 1000标记扫描行数较大这些标记规则本身也很适合反哺给插件提示词让插件知道风险点已经被工具侧识别到了模型只需要围绕风险点展开解释即可。5.3 注入业务语义表注释、字段注释与关系描述sakila 比较良心表和字段都有 COMMENT。字段层面rental表的rental_date注释是租赁日期return_date注释是归还日期。插件采集元数据时带上 COMMENT模型就能理解这些字段的业务含义给出的建议会更靠谱。SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, COLUMN_COMMENT FROM information_schema.COLUMNS WHERE TABLE_SCHEMA sakila AND TABLE_NAME IN (rental, customer, inventory);把这些语义描述压缩成一行一个字段的格式和控制 token 消耗是同一件事。比如 rental 表rental: rental_id PK | rental_date DATETIME 租赁日期 | inventory_id 库存ID | customer_id 客户ID | return_date DATETIME 归还日期 | staff_id 员工ID模型看到租赁日期这种语义描述就能理解查询每个客户最近租赁记录为什么要按 rental_date 排序也更容易想到rental_date 上有索引但联合查询时可能因为排序条件导致 filesort这类深层问题。6. 实测三条典型慢查询以及我踩过的四个坑6.1 三条慢查询的实测结果我在 sakila 上实测了三条典型慢查询插件输出摘要和手工核查结论如下。第一条统计每个分类下有多少部电影SELECT c.name, COUNT(f.film_id) AS film_cnt FROM category c LEFT JOIN film_category fc ON c.category_id fc.category_id LEFT JOIN film f ON fc.film_id f.film_id GROUP BY c.name;插件识别出 category 表驱动type 为 ALL扫描 16 行film_category 走 ref 索引film 走 PRIMARY 索引整体没有明显风险。结论是查询量小索引使用合理。手工验证后发现因为 film_category 有外键索引这个查询确实没什么优化空间插件判断正确。第二条查找被租赁次数最多的前 10 部电影SELECT f.title, COUNT(r.rental_id) AS rental_cnt FROM film f LEFT JOIN inventory i ON f.film_id i.film_id LEFT JOIN rental r ON i.inventory_id r.inventory_id GROUP BY f.title ORDER BY rental_cnt DESC LIMIT 10;插件标记了 inventory 表的 ALL 扫描和 Using temporary; Using filesort。这是个很有意思的案例——film 表 join inventory 时inventory 只有 4581 行全表扫描成本并不高但 GROUP BY 产生的临时表和 filesort 确有其事。模型的建议是可以给 inventory.film_id 建联合索引 (film_id, inventory_id) 覆盖查询但实际操作中 sakila 的 inventory 表已经有 idx_fk_film_id单从这个查询看收益有限。这里我看到模型的一个倾向它倾向于推荐建索引这种通用方案而不是先分析是否值得建。这个问题我后面会再提。第三条查询每位客户最近一次的租赁记录SELECT c.first_name, c.last_name, r.rental_date, r.return_date FROM customer c LEFT JOIN rental r ON c.customer_id r.customer_id WHERE r.rental_date ( SELECT MAX(r2.rental_date) FROM rental r2 WHERE r2.customer_id c.customer_id );插件识别出相关子查询导致 rental 表被驱动扫描两次access_type 为 ALL估算行数 16044出现 Using where。模型的优化建议是把相关子查询改写为窗口函数ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY rental_date DESC)并指出窗口函数在 MySQL 8.0 中可能比相关子查询更高效。我在 MySQL 里用 EXPLAIN ANALYZE 验证了一下窗口函数版本的执行计划确实消除了相关子查询的重复扫描整体耗时降低了约 60%。这个案例是插件价值最直观的体现。6.2 坑一DML 语句被误当成分析对象测试时我故意让插件分析一条 UPDATE结果模型正常触发了工具但 MySQL 8.0 对 UPDATE 执行 EXPLAIN 的返回结构和对 SELECT 很不一样导致解析报错。后来在工具函数入口加了 SQL 类型判断import sqlparse def validate_select(sql_text: str) - bool: parsed sqlparse.parse(sql_text) if not parsed: return False return parsed[0].get_type() SELECT非 SELECT 直接返回友好提示让模型转告用户插件只分析查询语句。这个校验很基础但能省掉大量解析错误。6.3 坑二频繁建连导致超时最开始每调一次工具就新建一个 pymysql 连接跑几次之后连接就堆起来了偶发MySQL Connection not available。改成dbutils.pooled_db.PooledDB连接池后问题消失。我设置 maxconnections5、blockingTrue、read_timeout5这样模型在连续调用工具时不会因为每次都握手而浪费时间也不会因为单条 EXPLAIN 卡住而拖死整个会话。6.4 坑三上下文 token 膨胀sakila 的单表 DDL 不长但真实生产的表动辄几十个字段、十几个索引。这时如果插件把 SHOW CREATE TABLE 的完整输出给模型一次就能吃掉几千 token。压缩策略有两个方向一是按条件裁剪只返回 EXPLAIN 里出现过的表的字段和索引二是把字段类型转成简写去掉 COMMENT 之外的非必要信息。我在采集元数据时用了 information_schema 查询只取列名、类型、注释、索引列控制在每条表 20 行以内。6.5 坑四rows 估算值被当成精确值模型拿到rows: 16044后会在诊断报告里写扫描了 16044 行。这是不对的。EXPLAIN 的 rows 是优化器估算值基于统计信息和实际扫描行数可能有数量级差异。我在特征提取时把字段名改成了rows_estimate同时加了显式提示这是估算值非精确计数。这算是在插件和模型之间约定了一个小小的数据契约实测下来模型在报告里就不再把 rows 当精确值了。7. 再往下走慢查询日志接入与定时巡检插件跑通之后我第一个想到的扩展方向是把它接到慢查询日志上。MySQL 的 slow_query_log 文件记录的都是真实业务里的慢 SQL把这些 SQL 定时收集起来批量丢给插件分析就形成了一个慢查询自动巡检闭环。实现不复杂用 cron 每分钟解析一次 slow log把 query_time 超过阈值的 SQL 提取出来逐条调用 analyze_sql_plan 工具最后把诊断报告推到消息通知渠道。这个扩展的难点不在插件而在慢日志的解析。MySQL 8.0 的慢日志格式里# Query_time: 2.315450这种行后面跟着SET timestamp...和实际 SQLSQL 可能跨多行需要按# Time:分隔符做块状解析。我建议用mysqldumpslow做预聚合先把同构 SQL 归一化再去重避免同一类 SQL 被分析几十遍。另一个扩展方向是把 EXPLAIN ANALYZE 集成进来。普通 EXPLAIN 只给估算值EXPLAIN ANALYZE 会真实执行 SQL 并返回实际耗时和实际行数。两者的差异能暴露优化器估算偏差的问题。不过 EXPLAIN ANALYZE 会真正执行 DML 语句所以我只对 SELECT 启用并且要在插件里增加一个是否允许真实执行的开关避免线上误操作。还有个方向是支持多库对比。我在插件里预留了 database 参数就是为了后续可以传一个库列表对同一模板的 SQL 在不同库上的执行计划做横向对比找出统计信息差距导致的性能差异。这个对运维排查为什么同样一条 SQL 在测试环境快、生产慢这类问题很有帮助。我在实际使用中的一个体会是这种工具自动采集、模型负责解读的组合最大的价值不是替代 DBA而是把 DBA 从读执行计划这种低层次劳动里解放出来让他们有精力去处理模型判断不了的事情比如业务数据分布变化、索引维护策略、硬件层面的瓶颈。插件能做的只是一道前菜真正的分析判断还是得靠人。最后分享一个小经验给模型的工具描述里写清楚这是估算值只读分析不修改数据这类信息比在代码里加一百条注释都管用。让模型明确知道工具的能力边界它才不会在边界外自由发挥。
返回列表