CRUD BOY实战(一)
验证你的SQL预编译生效了吗?
作业要求:
预编译定义:select * from sbtest1 where id=? 预编译就是将这个 SQL 模板发给 MySQL,将来要执行的时候只需要发送具体 id 值给 MySQL
优点:防止 SQL 注入;更好的性能
比如iBatis内置参数,形如#xxx#的,均采用了SQL预编译的形式:
那么 CRUD boy 这样写了就代表真正用上预编译了吗?也就是同一个 SQL 模板在不同的连接下真的用上PrepareStatement 了吗?
这次的问题是:你怎么确认这个预编译生效了?你如果让这个预编译真正生效?请对比一下预编译前后的性能差异你能扫描一下你们公司的业务,并确认预编译按照 crud的意愿正确生效了预编译的原理是?可以从代码层解释/也可以从SQL网络包层面来解释
- 你怎么确认这个预编译生效了?
- 你如果让这个预编译真正生效?
- 请对比一下预编译前后的性能差异
- 你能扫描一下你们公司的业务,并确认预编译按照 crud的意愿正确生效了
- 预编译的原理是?可以从代码层解释/也可以从SQL网络包层面来解释
实验背景
SQL预编译是一种将SQL语句结构和参数分开发送的技术。当使用预编译时,先发送包含占位符的PREPARE命令,MySQL服务器解析并优化这个语句。之后,只需发送EXECUTE命令和参数值,无需每次发送完整SQL语句。
预编译的主要优点:
- 防止SQL注入
- 提升查询性能
实验环境
- MacOS Sequoia 15.3
- WireShark 4.4.3
- MySQL 9.2.0 Homebrew
- Python 3.9.6
- mysql-connector-python 9.2.0 (Installed by pip3)
实验准备
1. 数据库初始化
CREATE DATABASE IF NOT EXISTS test_prepared;
USE test_prepared;
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255),
age INT
);
INSERT INTO users (name, age) VALUES ("Alice", 30);
INSERT INTO users (name, age) VALUES ("Bob", 25);
INSERT INTO users (name, age) VALUES ("Calvin", 35);
2. Wireshark配置
- 选择loopback:lo0接口
- 设置过滤器:
tcp port 3306 and host localhost - 配置MySQL解析器,启用"Reassemble MySQL Protocol"
- Mac用户需要先安装chmodBPF
实验一:预编译验证
代码实现
import mysql.connector
# MySQL 连接信息
cnx = mysql.connector.connect(
user = 'root',
password = '123456',
host = 'localhost',
database = 'test_prepared',
raise_on_warnings = True,
ssl_disabled=True # 禁用SSL以便Wireshark抓包
)
cursor = cnx.cursor()
# 1. 使用非预编译执行查询
age = 30
query_non_prepared = f"SELECT * FROM users WHERE age = {age}"
cursor.execute(query_non_prepared)
results = cursor.fetchall()
for row in results:
print(row)
# 2. 使用预编译执行查询
query_prepared = "SELECT * FROM users WHERE age = %s"
data_prepared = (25,)
cursor.execute(query_prepared, data_prepared)
results = cursor.fetchall()
for row in results:
print(row)
# 再次使用预编译,参数不同
data_prepared_2 = (35,)
cursor.execute(query_prepared, data_prepared_2)
results = cursor.fetchall()
for row in results:
print(row)
cursor.close()
cnx.close()
抓包分析
1. 连接过程
- TCP三次握手
- ServerGreeting初始化连接,包含MySQL版本信息
- LoginRequest请求登录
- AuthSwitchRequest验证身份
- 请求和响应交互
- RequestQUIT请求关闭连接
- TCP四次挥手
2. 非预编译查询
- 发送完整SQL查询语句
- 每次查询都需要重新解析优化
3. 预编译查询(启用prepared=True后)
- 发送带占位符的查询语句
- 生成Statement ID
- 执行时只发送参数值
- 使用已编译的模板
实验二:性能对比
代码实现
import mysql.connector
import time
# MySQL 连接信息
cnx = mysql.connector.connect(
user = 'root',
password = '123456',
host = 'localhost',
database = 'test_prepared',
raise_on_warnings = True,
ssl_disabled=True
)
cursor = cnx.cursor(prepared=True)
# 查询次数
num_iterations = 1000
# 1. 非预编译查询性能测试
total_time_non_prepared = 0
age_non_prepared_values = [30, 25, 35]
for i in range(num_iterations):
start_time = time.time()
age = age_non_prepared_values[i % len(age_non_prepared_values)]
query_non_prepared = f"SELECT * FROM users WHERE age = {age}"
cursor.execute(query_non_prepared)
cursor.fetchall()
end_time = time.time()
total_time_non_prepared += (end_time - start_time)
average_time_non_prepared = total_time_non_prepared / num_iterations
print(f"非预编译查询 {num_iterations} 次平均执行时间: {average_time_non_prepared:.6f} 秒")
# 2. 预编译查询性能测试
total_time_prepared = 0
age_prepared_values = [30, 25, 35]
query_prepared = "SELECT * FROM users WHERE age = %s"
for i in range(num_iterations):
start_time = time.time()
data_prepared = (age_prepared_values[i % len(age_prepared_values)],)
cursor.execute(query_prepared, data_prepared)
cursor.fetchall()
end_time = time.time()
total_time_prepared += (end_time - start_time)
average_time_prepared = total_time_prepared / num_iterations
print(f"预编译查询 {num_iterations} 次平均执行时间: {average_time_prepared:.6f} 秒")
cursor.close()
cnx.close()
测试结果
- 非预编译查询平均时间:0.000391秒
- 预编译查询平均时间:0.000243秒
实验结论
1. 预编译实现机制
- 首次执行:
- 发送SQL模板
- MySQL解析优化
- 生成Statement ID
- 后续执行:
- 仅发送参数值
- 复用已编译模板
2. 性能优势
- 避免重复解析SQL
- 减少网络传输量
- 服务器端复用执行计划
3. 实施建议
- 显式启用prepared=True
- 使用占位符代替直接值拼接
- 复杂查询更适合使用预编译
- 需权衡查询复杂度与预编译开销
4. 注意事项
- 配置连接参数(如ssl_disabled)
- 正确使用占位符语法
- 考虑连接池和预编译语句缓存