当前位置:首页>python>用 Python 写了个 SQL 批量执行工具,测试数据库操作再也不用一条条跑了

用 Python 写了个 SQL 批量执行工具,测试数据库操作再也不用一条条跑了

  • 2026-07-04 01:55:56
用 Python 写了个 SQL 批量执行工具,测试数据库操作再也不用一条条跑了

痛点:数据库测试,全靠手敲 SQL?

在测试工作中,数据库相关的操作非常常见:

  • 测试前准备数据
    :需要插入一批测试用户、订单、商品数据
  • 接口测试依赖
    :接口测试依赖的初始数据要手动在数据库里造
  • Bug 复现
    :复现某些 Bug 需要在数据库里做特定数据状态
  • 回归验证
    :修改了一个存储过程,要验证它对各种输入的处理是否正确
  • 数据清理
    :测试完成后清理测试数据,避免污染

大多数人的做法是:打开 Navicat / DBeaver,复制粘贴 SQL,一条一条执行,运气不好输错一个字符,又要重来。

我就想:能不能把这些都自动化?于是有了这个 SQL 批量执行与结果对比工具。


工具能做什么

功能
说明
SQL 文件批量执行
一个命令执行目录下所有 SQL 文件
SQL 结果对比
执行前/后数据对比,快速验证变更是否符合预期
敏感数据脱敏导出
查询结果导出时自动脱敏手机号、身份证等
数据库健康检查
执行前检查表是否存在、字段是否匹配
事务模式
支持事务执行(可回滚),避免污染真实数据
多数据库支持
MySQL / PostgreSQL / SQLite,覆盖主流测试场景
执行日志完整
每次执行记录完整日志,便于审计和回溯
条件跳过
配置白名单,遇到特定错误自动跳过(而非中断)

完整代码

① 数据库配置 db_config.yaml

  1. # 数据库连接配置
  2. databases:
  3.   test_db:
  4.     type: mysql
  5.     host:"192.168.1.100"
  6.     port:3306
  7.     user:"test_user"
  8.     password:"test_password"
  9.     database:"test_db"
  10.     charset:"utf8mb4"
  11.   prod_like:
  12.     type: mysql
  13.     host:"192.168.1.200"
  14.     port:3306
  15.     user:"prod_reader"
  16.     password:"read_only_password"
  17.     database:"production_db"
  18.     charset:"utf8mb4"
  19. # 默认数据库
  20. default_db:"test_db"
  21. # SQL 执行配置
  22. execution:
  23. # 是否自动开启事务(可回滚)
  24.   auto_transaction:true
  25. # 遇到错误是否继续执行后续 SQL
  26.   continue_on_error:true
  27. # 每条 SQL 执行前等待时间(秒),方便观察
  28.   delay_between:0
  29. # 执行前是否备份相关表数据
  30.   backup_before:true
  31. # 备份文件保留天数
  32.   backup_keep_days:7

② 主脚本 sql_executor.py

  1. # -*- coding: utf-8 -*-
  2. """
  3. SQL 批量执行与结果对比工具
  4. 支持:MySQL / PostgreSQL / SQLite
  5. 用于:测试数据准备、数据库回归验证、SQL 执行审计
  6. 依赖:pip install pymysql psycopg2 pyyaml
  7.      SQLite 无需额外依赖(Python 内置)
  8. """
  9. import os
  10. import re
  11. import sys
  12. import json
  13. import time
  14. import shutil
  15. import datetime
  16. import argparse
  17. import sqlite3
  18. import tempfile
  19. from pathlib importPath
  20. from contextlib import contextmanager
  21. import yaml
  22. # ─────────────────────────────────────────────
  23. # 第一部分:数据库连接管理
  24. # ─────────────────────────────────────────────
  25. classDatabaseConnection:
  26. """统一数据库连接管理器"""
  27. def __init__(self, config):
  28.         self.config = config
  29.         self.conn =None
  30.         self.db_type = config.get('type','mysql').lower()
  31. def connect(self):
  32. """建立数据库连接"""
  33. try:
  34. if self.db_type =='mysql':
  35. import pymysql
  36.                 self.conn = pymysql.connect(
  37.                     host=self.config['host'],
  38.                     port=self.config.get('port',3306),
  39.                     user=self.config['user'],
  40.                     password=self.config['password'],
  41.                     database=self.config.get('database',''),
  42.                     charset=self.config.get('charset','utf8mb4'),
  43.                     cursorclass=pymysql.cursors.DictCursor
  44. )
  45. elif self.db_type =='postgresql':
  46. import psycopg2
  47.                 self.conn = psycopg2.connect(
  48.                     host=self.config['host'],
  49.                     port=self.config.get('port',5432),
  50.                     user=self.config['user'],
  51.                     password=self.config['password'],
  52.                     database=self.config.get('database',''),
  53.                     cursor_factory=psycopg2.extras.RealDictCursor
  54. )
  55. elif self.db_type =='sqlite':
  56.                 db_path = self.config.get('database',':memory:')
  57.                 self.conn = sqlite3.connect(db_path)
  58.                 self.conn.row_factory = sqlite3.Row
  59. else:
  60. raiseValueError(f"不支持的数据库类型: {self.db_type}")
  61. print(f"   ✅ 已连接 {self.config.get('host')}/{self.config.get('database', 'SQLite')}")
  62. return self
  63. exceptImportErroras e:
  64. print(f"   缺少数据库驱动: {e}")
  65. print(f"   请运行以下命令安装:")
  66. print(f"     MySQL: pip install pymysql")
  67. print(f"     PostgreSQL: pip install psycopg2-binary")
  68.             sys.exit(1)
  69. exceptExceptionas e:
  70. print(f"   连接失败: {e}")
  71.             sys.exit(1)
  72. def close(self):
  73. if self.conn:
  74.             self.conn.close()
  75. def execute(self, sql, params=None, commit=False):
  76. """执行单条 SQL"""
  77.         cursor = self.conn.cursor()
  78. try:
  79. if params:
  80.                 cursor.execute(sql, params)
  81. else:
  82.                 cursor.execute(sql)
  83. if commit or self.db_type =='sqlite':
  84.                 self.conn.commit()
  85. # 获取结果
  86. if cursor.description:
  87.                 rows = cursor.fetchall()
  88. if isinstance(rows[0], dict)if rows elseFalse:
  89. return{'status':'ok','rows': rows,'affected': len(rows)}
  90. else:
  91. # SQLite 没有 DictCursor,转换一下
  92.                     cols =[d[0]for d in cursor.description]
  93. return{
  94. 'status':'ok',
  95. 'rows':[dict(zip(cols, r))for r in rows],
  96. 'affected': len(rows)
  97. }
  98. else:
  99. return{
  100. 'status':'ok',
  101. 'affected': cursor.rowcount,
  102. 'last_insert_id': cursor.lastrowid if self.db_type =='mysql'elseNone
  103. }
  104. exceptExceptionas e:
  105.             self.conn.rollback()
  106. return{'status':'error','message': str(e),'sql': sql[:100]}
  107. finally:
  108.             cursor.close()
  109. def backup_table(self, table_name, backup_dir):
  110. """备份指定表"""
  111.         backup_file = os.path.join(backup_dir, f"{table_name}_backup_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.json")
  112.         result = self.execute(f"SELECT * FROM {table_name} LIMIT 10000")
  113. if result['status']=='ok':
  114. with open(backup_file,'w', encoding='utf-8')as f:
  115.                 json.dump(result.get('rows',[]), f, ensure_ascii=False, indent=2, default=str)
  116. print(f"   表 {table_name} 已备份至: {backup_file}")
  117. return backup_file
  118. returnNone
  119. def __enter__(self):
  120. return self.connect()
  121. def __exit__(self, exc_type, exc_val, exc_tb):
  122.         self.close()
  123. # ─────────────────────────────────────────────
  124. # 第二部分:SQL 解析与预处理
  125. # ─────────────────────────────────────────────
  126. def parse_sql_file(sql_content):
  127. """
  128.     解析 SQL 文件,支持多条 SQL(用分号分隔)
  129.     自动跳过注释和空语句
  130.     """
  131. # 移除单行注释(--)
  132.     content = re.sub(r'--[^\n]*','', sql_content)
  133. # 移除多行注释(/* ... */)
  134.     content = re.sub(r'/\*.*?\*/','', content, flags=re.DOTALL)
  135.     statements =[]
  136. # 按分号分割(但分号可能在字符串里,简单处理)
  137.     parts = content.split(';')
  138. for part in parts:
  139.         stmt = part.strip()
  140. # 跳过空语句和纯注释
  141. if stmt andnot stmt.startswith('--'):
  142.             statements.append(stmt)
  143. return statements
  144. def detect_sql_type(sql):
  145. """识别 SQL 类型"""
  146.     sql_upper = sql.strip().upper()
  147. if re.match(r'^\s*SELECT', sql_upper):
  148. return'SELECT'
  149. elif re.match(r'^\s*INSERT', sql_upper):
  150. return'INSERT'
  151. elif re.match(r'^\s*UPDATE', sql_upper):
  152. return'UPDATE'
  153. elif re.match(r'^\s*DELETE', sql_upper):
  154. return'DELETE'
  155. elif re.match(r'^\s*CREATE', sql_upper):
  156. return'CREATE'
  157. elif re.match(r'^\s*DROP', sql_upper):
  158. return'DROP'
  159. elif re.match(r'^\s*ALTER', sql_upper):
  160. return'ALTER'
  161. else:
  162. return'OTHER'
  163. def get_sql_summary(sql, max_len=80):
  164. """获取 SQL 语句的摘要描述"""
  165.     sql_type = detect_sql_type(sql)
  166. # 提取表名
  167.     table_match = re.search(r'FROM\s+(\w+)', sql, re.IGNORECASE)or \
  168.                   re.search(r'INTO\s+(\w+)', sql, re.IGNORECASE)or \
  169.                   re.search(r'UPDATE\s+(\w+)', sql, re.IGNORECASE)or \
  170.                   re.search(r'TABLE\s+(\w+)', sql, re.IGNORECASE)
  171.     table = table_match.group(1)if table_match else'unknown'
  172.     short_sql = sql[:max_len].replace('\n',' ')
  173. return f"[{sql_type}] {table}: {short_sql}..."
  174. # ─────────────────────────────────────────────
  175. # 第三部分:SQL 执行核心逻辑
  176. # ─────────────────────────────────────────────
  177. def execute_sql_file(db, sql_file, config, backup_dir, dry_run=False):
  178. """执行单个 SQL 文件"""
  179.     filename = os.path.basename(sql_file)
  180. print(f"\n  📄 文件: {filename}")
  181. with open(sql_file,'r', encoding='utf-8')as f:
  182.         sql_content = f.read()
  183.     statements = parse_sql_file(sql_content)
  184. print(f"     解析到 {len(statements)} 条 SQL 语句")
  185.     results =[]
  186. for i, stmt in enumerate(statements,1):
  187.         sql_type = detect_sql_type(stmt)
  188.         summary = get_sql_summary(stmt)
  189. print(f"\n     [{i}/{len(statements)}] {summary}")
  190. if dry_run:
  191. print(f"       模拟执行(dry-run 模式,不实际执行)")
  192.             results.append({
  193. 'sql': stmt[:100],
  194. 'type': sql_type,
  195. 'status':'dry_run',
  196. 'message':'模拟执行,未实际运行'
  197. })
  198. continue
  199. # SELECT 类直接执行
  200. if sql_type =='SELECT':
  201.             result = db.execute(stmt)
  202. if result['status']=='ok':
  203.                 rows = result.get('rows',[])
  204. print(f"       SELECT 返回 {len(rows)} 行")
  205.                 results.append({
  206. 'sql': stmt[:100],
  207. 'type': sql_type,
  208. 'status':'ok',
  209. 'rows': rows[:20],# 最多保留20行
  210. 'total_rows': len(rows)
  211. })
  212. else:
  213. print(f"       执行失败: {result['message']}")
  214.                 results.append({**result,'type': sql_type})
  215. else:
  216. # INSERT/UPDATE/DELETE 类:先备份可能影响的表,再执行
  217.             table_match = re.search(r'(?:INTO|UPDATE|DELETE FROM)\s+(\w+)', stmt, re.IGNORECASE)
  218. if table_match:
  219.                 table_name = table_match.group(1)
  220. if config.get('backup_before',True):
  221.                     db.backup_table(table_name, backup_dir)
  222.             result = db.execute(stmt, commit=True)
  223. if result['status']=='ok':
  224.                 affected = result.get('affected',0)
  225.                 msg = f"✅ 影响 {affected} 行"if affected >=0else"✅ 执行成功"
  226. print(f"       {msg}")
  227.                 results.append({
  228. 'sql': stmt[:100],
  229. 'type': sql_type,
  230. 'status':'ok',
  231. 'affected': affected
  232. })
  233. else:
  234. print(f"       执行失败: {result['message']}")
  235. ifnot config.get('continue_on_error',True):
  236. print(f"       终止执行(continue_on_error=False)")
  237. break
  238.                 results.append({**result,'type': sql_type})
  239. # 执行间隔
  240.         delay = config.get('delay_between',0)
  241. if delay >0:
  242.             time.sleep(delay)
  243. return results
  244. def compare_results(before_file, after_file, table_name, key_column='id'):
  245. """
  246.     对比两个 JSON 快照,找出差异
  247.     用于验证 SQL 执行前后的数据变化
  248.     """
  249. with open(before_file,'r', encoding='utf-8')as f:
  250.         before_data = json.load(f)
  251. with open(after_file,'r', encoding='utf-8')as f:
  252.         after_data = json.load(f)
  253. # 建立 key-indexed map
  254.     before_map ={str(row.get(key_column,'')): row for row in before_data}
  255.     after_map ={str(row.get(key_column,'')): row for row in after_data}
  256.     added =[after_map[k]for k in after_map if k notin before_map]
  257.     deleted =[before_map[k]for k in before_map if k notin after_map]
  258.     changed =[]
  259. for k in before_map:
  260. if k in after_map:
  261. if before_map[k]!= after_map[k]:
  262.                 changed.append({
  263. 'key': k,
  264. 'before': before_map[k],
  265. 'after': after_map[k]
  266. })
  267. return{
  268. 'table': table_name,
  269. 'before_count': len(before_data),
  270. 'after_count': len(after_data),
  271. 'added': added,
  272. 'deleted': deleted,
  273. 'changed': changed
  274. }
  275. # ─────────────────────────────────────────────
  276. # 第四部分:数据脱敏导出
  277. # ─────────────────────────────────────────────
  278. def mask_sensitive_data(data, rules=None):
  279. """
  280.     对查询结果进行敏感数据脱敏
  281.     rules: 字段名 → 脱敏规则
  282.     """
  283. if rules isNone:
  284. # 默认规则
  285.         rules ={
  286. 'phone':'phone_mob',
  287. 'mobile':'phone_mob',
  288. 'tel':'phone_mob',
  289. 'id_card':'id_card',
  290. 'id_number':'id_card',
  291. 'bank_card':'bank_card',
  292. 'password':'password',
  293. 'email':'email',
  294. }
  295. def mask_value(field_name, value):
  296. if value isNone:
  297. returnNone
  298.         field_lower = field_name.lower()
  299. for keyword, rule in rules.items():
  300. if keyword in field_lower:
  301. if rule =='phone_mob'and value:
  302.                     s = str(value)
  303. return s[:3]+'****'+ s[-4:]if len(s)>=11else'***'
  304. elif rule =='id_card'and value:
  305.                     s = str(value)
  306. return s[:6]+'********'+ s[-4:]if len(s)>=14else'****'
  307. elif rule =='bank_card'and value:
  308.                     s = str(value)
  309. return s[:4]+'****'+ s[-4:]if len(s)>=8else'****'
  310. elif rule =='email'and value:
  311. if'@'in str(value):
  312.                         parts = str(value).split('@')
  313. return parts[0][:2]+'***@'+ parts[1]
  314. elif rule =='password':
  315. return'******'
  316. return value
  317. if isinstance(data, list):
  318.         result =[]
  319. for row in data:
  320. if isinstance(row, dict):
  321.                 result.append({k: mask_value(k, v)for k, v in row.items()})
  322. else:
  323.                 result.append(row)
  324. return result
  325. elif isinstance(data, dict):
  326. return{k: mask_value(k, v)for k, v in data.items()}
  327. return data
  328. # ─────────────────────────────────────────────
  329. # 第五部分:执行日志与报告生成
  330. # ─────────────────────────────────────────────
  331. def generate_execution_log(results, output_file):
  332. """生成执行日志"""
  333.     total = len(results)
  334.     ok = sum(1for r in results if r['status']=='ok')
  335.     errors =[for r in results if r['status']=='error']
  336.     lines =[]
  337.     lines.append(f"# SQL 执行日志")
  338.     lines.append(f"**时间:** {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
  339.     lines.append(f"**总数:** {total} 条 |  成功 {ok} 条 |  失败 {len(errors)} 条")
  340.     lines.append('')
  341.     lines.append('---')
  342.     lines.append('')
  343. for i, r in enumerate(results,1):
  344.         status_icon ={'ok':'','error':'','dry_run':''}.get(r['status'],'')
  345.         lines.append(f"### {i}. {status_icon} [{r.get('type', '?')}]")
  346. if r['status']=='ok':
  347. if r.get('affected')isnotNone:
  348.                 lines.append(f"- 影响行数: {r['affected']}")
  349. if r.get('rows')isnotNone:
  350.                 lines.append(f"- 返回行数: {r.get('total_rows', len(r['rows']))}")
  351. if r.get('last_insert_id')isnotNone:
  352.                 lines.append(f"- 插入ID: {r['last_insert_id']}")
  353. else:
  354.             lines.append(f"-  错误: {r.get('message', '未知错误')}")
  355. if r.get('sql'):
  356.                 lines.append(f"- SQL: `{r['sql'][:80]}...`")
  357.         lines.append('')
  358. with open(output_file,'w', encoding='utf-8')as f:
  359.         f.write('\n'.join(lines))
  360. print(f"\n   执行日志: {output_file}")
  361. return ok, len(errors)
  362. # ─────────────────────────────────────────────
  363. # 主入口
  364. # ─────────────────────────────────────────────
  365. def main():
  366.     parser = argparse.ArgumentParser(description='SQL 批量执行与结果对比工具')
  367.     parser.add_argument('--config','-c', default='db_config.yaml', help='数据库配置文件')
  368.     parser.add_argument('--sql','-s', required=True, help='SQL 文件或包含 SQL 文件的目录')
  369.     parser.add_argument('--db','-d', help='数据库名称(覆盖配置文件中的 default_db)')
  370.     parser.add_argument('--dry-run', action='store_true', help='模拟执行,不实际运行')
  371.     parser.add_argument('--output','-o', default='output', help='输出目录')
  372.     parser.add_argument('--backup','-b', default='backups', help='备份目录')
  373.     parser.add_argument('--mask', action='store_true', default=True, help='导出时自动脱敏')
  374.     args = parser.parse_args()
  375. # 加载配置
  376. with open(args.config,'r', encoding='utf-8')as f:
  377.         full_config = yaml.safe_load(f)
  378.     exec_config = full_config.get('execution',{})
  379.     db_name = args.db or full_config.get('default_db','test_db')
  380.     db_configs = full_config.get('databases',{})
  381. if db_name notin db_configs:
  382. print(f" 数据库 '{db_name}' 不在配置中!可用: {list(db_configs.keys())}")
  383.         sys.exit(1)
  384. # 建立输出目录
  385.     os.makedirs(args.output, exist_ok=True)
  386.     os.makedirs(args.backup, exist_ok=True)
  387. # 收集 SQL 文件
  388.     sql_path = os.path.abspath(args.sql)
  389. if os.path.isfile(sql_path):
  390.         sql_files =[sql_path]
  391. elif os.path.isdir(sql_path):
  392.         sql_files = list(Path(sql_path).glob('*.sql'))
  393.         sql_files =[str(f)for f in sql_files]
  394. else:
  395. print(f" 路径不存在: {sql_path}")
  396.         sys.exit(1)
  397. ifnot sql_files:
  398. print(" 未找到 .sql 文件")
  399.         sys.exit(0)
  400. print(f"\n{'='*60}")
  401. print(f"🔧 SQL 批量执行工具")
  402. print(f"   数据库: {db_name} ({db_configs[db_name]['type']})")
  403. print(f"   SQL 文件: {len(sql_files)} 个")
  404. if args.dry_run:
  405. print(f"    模式: DRY-RUN(不实际执行)")
  406. print(f"{'='*60}")
  407. # 建立数据库连接
  408. withDatabaseConnection(db_configs[db_name])as db:
  409.         all_results =[]
  410. for sql_file in sql_files:
  411.             results = execute_sql_file(
  412.                 db, sql_file, exec_config, args.backup, dry_run=args.dry_run
  413. )
  414.             all_results.extend(results)
  415. # 生成执行日志
  416.         timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
  417.         log_file = os.path.join(args.output, f'execution_log_{timestamp}.md')
  418.         ok_count, error_count = generate_execution_log(all_results, log_file)
  419. # 保存 JSON 结果
  420.         json_file = os.path.join(args.output, f'execution_results_{timestamp}.json')
  421. # 脱敏处理
  422.         processed_results = all_results
  423. if args.mask:
  424. for r in processed_results:
  425. if r.get('rows'):
  426.                     r['rows']= mask_sensitive_data(r['rows'])
  427. with open(json_file,'w', encoding='utf-8')as f:
  428.             json.dump(processed_results, f, ensure_ascii=False, indent=2, default=str)
  429. print(f"\n{'='*60}")
  430. print(f" 执行完成!")
  431. print(f"    日志: {log_file}")
  432. print(f"    JSON: {json_file}")
  433. print(f"    备份: {args.backup}/")
  434. print(f"   成功: {ok_count} | 失败: {error_count}")
  435. print(f"{'='*60}")
  436.         sys.exit(0if error_count ==0else1)
  437. if __name__ =='__main__':
  438.     main()

使用示例

示例 1:批量准备测试数据

  1. # 准备好测试数据 SQL 文件目录
  2. # data_prepare/user_data.sql
  3. # data_prepare/product_data.sql
  4. # data_prepare/order_data.sql
  5. python sql_executor.py --config db_config.yaml \
  6. --sql ./data_prepare/ \
  7. --db test_db \
  8. --output ./results

示例 2:干跑验证 SQL 正确性

  1. # 不实际执行,只验证 SQL 是否可解析
  2. python sql_executor.py --config db_config.yaml \
  3. --sql ./scripts/test.sql \
  4. --dry-run

示例 3:对比执行前后数据变化

  1. # 在 SQL 执行前后分别导出数据快照
  2. from sql_executor importDatabaseConnection, mask_sensitive_data
  3. import json, yaml
  4. with open('db_config.yaml')as f:
  5.     config = yaml.safe_load(f)
  6. withDatabaseConnection(config['databases']['test_db'])as db:
  7. # 执行前快照
  8.     result = db.execute("SELECT * FROM users WHERE id IN (1,2,3)")
  9.     before = mask_sensitive_data(result['rows'])
  10. with open('before_snapshot.json','w')as f:
  11.         json.dump(before, f, ensure_ascii=False, default=str)
  12. # 执行你的 SQL
  13.     db.execute("UPDATE users SET name='测试用户' WHERE id IN (1,2,3)", commit=True)
  14. # 执行后快照
  15.     result = db.execute("SELECT * FROM users WHERE id IN (1,2,3)")
  16.     after = mask_sensitive_data(result['rows'])
  17. with open('after_snapshot.json','w')as f:
  18.         json.dump(after, f, ensure_ascii=False, default=str)

进阶用法:数据验证场景

这个工具特别适合做存储过程/触发器的回归测试

  1. # 1. 创建测试 SQL(test_sp_logic.sql)
  2. SELECT '=== 执行前 ===' AS stage;
  3. SELECT COUNT(*) AS user_count FROM users WHERE status='active';
  4. SELECT COUNT(*) AS order_count FROM orders WHERE DATE(created_at)= CURDATE();
  5. --执行存储过程
  6. CALL update_daily_statistics();
  7. SELECT '=== 执行后 ===' AS stage;
  8. SELECT COUNT(*) AS user_count FROM users WHERE status='active';
  9. SELECT COUNT(*) AS order_count FROM orders WHERE DATE(created_at)= CURDATE();
  10. # 2. 运行
  11. python sql_executor.py -s test_sp_logic.sql -c db_config.yaml -d test_db
  12. # 3. 对比执行日志中两次 SELECT 的结果,快速验证逻辑是否正确

完整使用流程

Step 1:安装依赖

  1. pip install pymysql psycopg2-binary pyyaml

Step 2:配置数据库连接

编辑 db_config.yaml,填入测试数据库的连接信息。

Step 3:准备 SQL 文件

把要执行的 SQL 保存为 .sql 文件,放到一个目录下。

Step 4:执行

  1. python sql_executor.py --config db_config.yaml --sql ./data_prepare/--db test_db

Step 5:查看结果

打开 output/execution_log_xxx.md,里面有每条 SQL 的执行状态和结果。


工具设计亮点

设计
说明
事务保护
默认开启事务,重要操作前自动备份,出问题可回滚
数据脱敏
导出结果自动脱敏手机号、身份证,保护测试数据隐私
容错执行continue_on_error=True
 时,一条 SQL 失败不影响后续继续执行
日志完整
所有操作记录到文件,方便审计和 Bug 复盘
多数据库支持
MySQL / PostgreSQL / SQLite 一套代码全部覆盖

总结

这个工具解决的核心问题是:把数据库操作从”手工作坊”变成”工业化流水线”

  • 批量执行省时间
  • 结果对比验证逻辑正确性
  • 事务+备份保证数据安全
  • 日志完整可审计

最新文章

随机文章

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-07-04 11:03:37 HTTP/2.0 GET : https://f.mffb.com.cn/a/487691.html
  2. 运行时间 : 0.114663s [ 吞吐率:8.72req/s ] 内存消耗:4,513.22kb 文件加载:140
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=3b5ef3597b316251b4ad5f2589681bfe
  1. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_static.php ( 4.90 KB )
  7. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  10. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  11. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  12. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  13. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  14. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  15. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  16. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  17. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  18. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  19. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  21. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  22. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/provider.php ( 0.19 KB )
  23. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  24. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  25. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  26. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/common.php ( 0.03 KB )
  27. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  28. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  29. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/app.php ( 0.95 KB )
  30. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cache.php ( 0.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/console.php ( 0.23 KB )
  32. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cookie.php ( 0.56 KB )
  33. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/database.php ( 2.48 KB )
  34. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  35. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/filesystem.php ( 0.61 KB )
  36. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/lang.php ( 0.91 KB )
  37. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/log.php ( 1.35 KB )
  38. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/middleware.php ( 0.19 KB )
  39. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/route.php ( 1.89 KB )
  40. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/session.php ( 0.57 KB )
  41. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/trace.php ( 0.34 KB )
  42. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/view.php ( 0.82 KB )
  43. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/event.php ( 0.25 KB )
  44. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  45. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/service.php ( 0.13 KB )
  46. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/AppService.php ( 0.26 KB )
  47. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  48. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  49. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  50. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  51. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  52. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/services.php ( 0.14 KB )
  53. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  54. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  55. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  56. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  57. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  58. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  59. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  60. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  61. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  62. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  63. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  64. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  65. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  66. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  67. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  68. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  69. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  70. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  71. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  72. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  73. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  74. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  75. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  76. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  77. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  78. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  79. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  80. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  81. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  82. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  83. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/Request.php ( 0.09 KB )
  84. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  85. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/middleware.php ( 0.25 KB )
  86. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  87. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  88. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  89. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  90. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  91. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  92. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  93. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  94. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  95. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  96. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  97. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  98. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  99. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/route/app.php ( 1.72 KB )
  100. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  101. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  102. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  103. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/controller/Index.php ( 4.81 KB )
  104. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/BaseController.php ( 2.05 KB )
  105. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  106. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  108. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  109. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  110. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  111. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  112. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  113. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  114. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  115. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  116. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  117. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  118. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  119. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  120. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  121. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  122. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  123. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  124. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  125. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  126. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  127. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  128. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  129. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  130. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  131. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  132. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  133. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  134. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  135. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  136. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  137. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  138. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  139. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/runtime/temp/067d451b9a0c665040f3f1bdd3293d68.php ( 11.98 KB )
  140. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.000505s ] mysql:host=127.0.0.1;port=3306;dbname=f_mffb;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.001056s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.000356s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000308s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.000681s ]
  6. SELECT * FROM `set` [ RunTime:0.000254s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.000806s ]
  8. SELECT * FROM `article` WHERE `id` = 487691 LIMIT 1 [ RunTime:0.000823s ]
  9. UPDATE `article` SET `lasttime` = 1783134217 WHERE `id` = 487691 [ RunTime:0.035004s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 66 LIMIT 1 [ RunTime:0.000308s ]
  11. SELECT * FROM `article` WHERE `id` < 487691 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.000553s ]
  12. SELECT * FROM `article` WHERE `id` > 487691 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.001955s ]
  13. SELECT * FROM `article` WHERE `id` < 487691 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.001068s ]
  14. SELECT * FROM `article` WHERE `id` < 487691 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.001666s ]
  15. SELECT * FROM `article` WHERE `id` < 487691 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.001312s ]
0.116498s