MySQL 传输协议精讲 / 06 - 文本协议
第 06 章:文本协议
6.1 文本协议概述
**文本协议(Text Protocol)**是 MySQL 最基础的结果集传输格式。当客户端通过 COM_QUERY 执行 SQL 语句时,服务器使用文本协议返回结果。
文本协议 vs 二进制协议
| 特性 | 文本协议 | 二进制协议 |
|---|---|---|
| 使用场景 | COM_QUERY | COM_STMT_EXECUTE |
| 值编码 | 字符串 | 二进制原生格式 |
| NULL 表示 | 0xFB 长度编码 | null_bitmap 中的 bit |
| 类型转换 | 服务端转为文本 | 客户端按类型解析 |
| 性能 | 较低(需要序列化/反序列化) | 较高 |
| 适用场景 | 简单查询、交互式工具 | 高性能应用、预处理语句 |
6.2 文本结果集结构
一个文本结果集由以下部分组成,按顺序传输:
1. Column Count (1 个包) ← 列数
2. Column Definition (N 个包) ← 每列的定义
3. [EOF] 或 OK (1 个包) ← 列定义结束标志
4. Row Data (M × 1 个包) ← 每行的数据
5. [EOF] 或 OK (1 个包) ← 行数据结束标志
客户端发送: COM_QUERY "SELECT id, name, email FROM users LIMIT 2"
服务器返回:
[Packet 1] Column Count = 3
[Packet 2] Column Definition: id
[Packet 3] Column Definition: name
[Packet 4] Column Definition: email
[Packet 5] EOF
[Packet 6] Row Data: 1, "Alice", "[email protected]"
[Packet 7] Row Data: 2, "Bob", "[email protected]"
[Packet 8] EOF
6.3 Column Count 包
第一个包告诉客户端结果集有多少列。
格式
内容: 长度编码的整数 (Length-Encoded Integer)
示例 (3 列):
03 → 长度编码: 值 3 (单字节)
def parse_column_count(payload: bytes) -> int:
"""解析列数"""
if payload[0] < 0xFB:
return payload[0]
elif payload[0] == 0xFC:
return struct.unpack('<H', payload[1:3])[0]
# ... 其他情况
6.4 Column Definition 包(Column 41)
每个列都有一个独立的定义包,描述列的名称、类型等元数据。
数据包结构
偏移量 大小 字段 说明
────────────────────────────────────────────────────
0 变长 catalog 固定 "def" (长度编码)
变长 schema 数据库名 (长度编码)
变长 table_alias 表别名 (长度编码)
变长 table_name 真实表名 (长度编码)
变长 column_alias 列别名 (长度编码)
变长 column_name 真实列名 (长度编码)
1 字节 0x0C (length of fixed fields) 固定长度标记
2 字节 character_set 字符集 ID
4 字节 column_length 列最大长度(字节)
1 字节 column_type 列类型 ID
2 字节 flags 列标志
1 字节 decimals 小数位数
2 字节 0x00 0x00 填充(保留)
字段类型 ID
| 类型 ID | 名称 | 说明 |
|---|---|---|
| 0x00 | MYSQL_TYPE_DECIMAL | 定点数 |
| 0x01 | MYSQL_TYPE_TINYINT | TINYINT |
| 0x02 | MYSQL_TYPE_SMALLINT | SMALLINT |
| 0x03 | MYSQL_TYPE_INT | INT |
| 0x04 | MYSQL_TYPE_FLOAT | FLOAT |
| 0x05 | MYSQL_TYPE_DOUBLE | DOUBLE |
| 0x06 | MYSQL_TYPE_NULL | NULL |
| 0x07 | MYSQL_TYPE_TIMESTAMP | TIMESTAMP |
| 0x08 | MYSQL_TYPE_BIGINT | BIGINT |
| 0x09 | MYSQL_TYPE_MEDIUMINT | MEDIUMINT |
| 0x0A | MYSQL_TYPE_DATE | DATE |
| 0x0B | MYSQL_TYPE_TIME | TIME |
| 0x0C | MYSQL_TYPE_DATETIME | DATETIME |
| 0x0D | MYSQL_TYPE_YEAR | YEAR |
| 0x0F | MYSQL_TYPE_VARCHAR | VARCHAR |
| 0x10 | MYSQL_TYPE_BIT | BIT |
| 0xF5 | MYSQL_TYPE_JSON | JSON |
| 0xF6 | MYSQL_TYPE_NEWDECIMAL | DECIMAL (新) |
| 0xF7 | MYSQL_TYPE_ENUM | ENUM |
| 0xF8 | MYSQL_TYPE_SET | SET |
| 0xF9 | MYSQL_TYPE_TINY_BLOB | TINYBLOB/TINYTEXT |
| 0xFA | MYSQL_TYPE_MEDIUM_BLOB | MEDIUMBLOB/MEDIUMTEXT |
| 0xFB | MYSQL_TYPE_LONG_BLOB | LONGBLOB/LONGTEXT |
| 0xFC | MYSQL_TYPE_BLOB | BLOB/TEXT |
| 0xFD | MYSQL_TYPE_VAR_STRING | VARCHAR/VARBINARY |
| 0xFE | MYSQL_TYPE_STRING | CHAR/BINARY |
| 0xFF | MYSQL_TYPE_GEOMETRY | 空间数据 |
列标志(Flags)
| 标志 | 值 | 说明 |
|---|---|---|
NOT_NULL_FLAG | 0x0001 | NOT NULL |
PRI_KEY_FLAG | 0x0002 | 主键 |
UNIQUE_KEY_FLAG | 0x0004 | 唯一键 |
MULTIPLE_KEY_FLAG | 0x0008 | 非唯一索引 |
BLOB_FLAG | 0x0010 | BLOB/TEXT |
UNSIGNED_FLAG | 0x0020 | UNSIGNED |
ZEROFILL_FLAG | 0x0040 | ZEROFILL |
BINARY_FLAG | 0x0080 | BINARY |
ENUM_FLAG | 0x0100 | ENUM |
AUTO_INCREMENT_FLAG | 0x0200 | AUTO_INCREMENT |
TIMESTAMP_FLAG | 0x0400 | TIMESTAMP |
SET_FLAG | 0x0800 | SET |
NO_DEFAULT_VALUE_FLAG | 0x1000 | 无默认值 |
ON_UPDATE_NOW_FLAG | 0x2000 | ON UPDATE CURRENT_TIMESTAMP |
Python 解析实现
"""
mysql_column_definition.py
解析 Column Definition 数据包
"""
import struct
from dataclasses import dataclass, field
from typing import Dict
@dataclass
class ColumnDefinition:
"""列定义"""
catalog: str = "def"
schema: str = ""
table_alias: str = ""
table_name: str = ""
column_alias: str = ""
column_name: str = ""
character_set: int = 0
column_length: int = 0
column_type: int = 0
flags: int = 0
decimals: int = 0
# 类型名称映射
COLUMN_TYPE_NAMES: Dict[int, str] = {
0x00: "DECIMAL", 0x01: "TINYINT", 0x02: "SMALLINT",
0x03: "INT", 0x04: "FLOAT", 0x05: "DOUBLE",
0x06: "NULL", 0x07: "TIMESTAMP", 0x08: "BIGINT",
0x09: "MEDIUMINT", 0x0A: "DATE", 0x0B: "TIME",
0x0C: "DATETIME", 0x0D: "YEAR", 0x0F: "VARCHAR",
0x10: "BIT", 0xF5: "JSON", 0xF6: "NEWDECIMAL",
0xF7: "ENUM", 0xF8: "SET", 0xF9: "TINYBLOB",
0xFA: "MEDIUMBLOB",0xFB: "LONGBLOB", 0xFC: "BLOB",
0xFD: "VAR_STRING",0xFE: "STRING", 0xFF: "GEOMETRY",
}
# 字符集名称映射
CHARSET_NAMES: Dict[int, str] = {
8: "latin1", 33: "utf8mb3", 45: "utf8mb4", 255: "utf8mb4",
63: "binary",
}
def read_length_encoded_str(data: bytes, offset: int) -> tuple:
"""读取长度编码字符串"""
length, offset = read_length_encoded_int(data, offset)
value = data[offset:offset + length].decode('utf-8', errors='replace')
return value, offset + length
def read_length_encoded_int(data: bytes, offset: int) -> tuple:
"""读取长度编码整数"""
first = data[offset]
if first < 0xFB:
return first, offset + 1
elif first == 0xFC:
return struct.unpack('<H', data[offset+1:offset+3])[0], offset + 3
elif first == 0xFD:
return struct.unpack('<I', data[offset+1:offset+4] + b'\x00')[0], offset + 4
elif first == 0xFE:
return struct.unpack('<Q', data[offset+1:offset+9])[0], offset + 9
def parse_column_definition(payload: bytes) -> ColumnDefinition:
"""解析 Column Definition 数据包"""
col = ColumnDefinition()
offset = 0
# catalog (长度编码字符串, 固定为 "def")
col.catalog, offset = read_length_encoded_str(payload, offset)
# schema (数据库名)
col.schema, offset = read_length_encoded_str(payload, offset)
# table_alias (表别名)
col.table_alias, offset = read_length_encoded_str(payload, offset)
# table_name (真实表名)
col.table_name, offset = read_length_encoded_str(payload, offset)
# column_alias (列别名)
col.column_alias, offset = read_length_encoded_str(payload, offset)
# column_name (真实列名)
col.column_name, offset = read_length_encoded_str(payload, offset)
# 固定长度字段的长度标记 (0x0C = 12)
fixed_len = payload[offset]
offset += 1
# character_set (2 字节)
col.character_set = struct.unpack('<H', payload[offset:offset+2])[0]
offset += 2
# column_length (4 字节)
col.column_length = struct.unpack('<I', payload[offset:offset+4])[0]
offset += 4
# column_type (1 字节)
col.column_type = payload[offset]
offset += 1
# flags (2 字节)
col.flags = struct.unpack('<H', payload[offset:offset+2])[0]
offset += 2
# decimals (1 字节)
col.decimals = payload[offset]
return col
def format_column(col: ColumnDefinition) -> str:
"""格式化列定义为可读字符串"""
type_name = COLUMN_TYPE_NAMES.get(col.column_type, f"UNKNOWN(0x{col.column_type:02X})")
charset = CHARSET_NAMES.get(col.character_set, f"charset({col.character_set})")
flags_str = []
if col.flags & 0x0001: flags_str.append("NOT_NULL")
if col.flags & 0x0002: flags_str.append("PRI_KEY")
if col.flags & 0x0020: flags_str.append("UNSIGNED")
if col.flags & 0x0200: flags_str.append("AUTO_INC")
return (
f"{col.schema}.{col.table_name}.{col.column_name} "
f"({type_name}, {charset}, len={col.column_length}, "
f"dec={col.decimals}, flags=[{','.join(flags_str)}])"
)
# 演示
def demo():
print("Column Definition 解析演示")
print()
# 模拟解析结果
columns = [
ColumnDefinition(
schema="test_db", table_name="users", column_name="id",
column_alias="id", character_set=63, column_length=11,
column_type=0x03, flags=0x0001 | 0x0002 | 0x0200, decimals=0
),
ColumnDefinition(
schema="test_db", table_name="users", column_name="name",
column_alias="name", character_set=45, column_length=255,
column_type=0xFD, flags=0x0000, decimals=0
),
ColumnDefinition(
schema="test_db", table_name="users", column_name="balance",
column_alias="balance", character_set=63, column_length=14,
column_type=0xF6, flags=0x0020, decimals=2
),
]
for i, col in enumerate(columns):
print(f" 列 {i}: {format_column(col)}")
if __name__ == '__main__':
demo()
6.5 EOF 包
EOF 包用于标记列定义和行数据的结束。
格式(传统)
字节偏移 大小 字段
──────────────────────────────
0 1 字节 0xFE (EOF 标识)
1 2 字节 warnings (警告数)
3 2 字节 status_flags (服务器状态)
总长度 = 5 字节。判定规则:首字节为 0xFE 且 payload 长度小于 9。
注意:MySQL 8.0 引入
CLIENT_DEPRECATE_EOF能力标志后,可以用 OK 包替代 EOF 包,减少一个字节。
6.6 Row Data 包
每一行数据是一个独立的数据包,其中各列值按顺序排列。
行数据格式
[列值1] [列值2] [列值3] ... [列值N]
每个列值使用长度编码格式:
| 编码 | 含义 |
|---|---|
| 0xFB | NULL 值 |
| 其他长度编码 | 字符串值(即使数值类型也以文本表示) |
示例
-- 查询结果
SELECT id, name, email FROM users LIMIT 2;
-- 结果:
-- | id | name | email |
-- | 1 | Alice | [email protected] |
-- | 2 | NULL | [email protected] |
Row 1:
列1 (id): 01 31 → "1" (长度编码: len=1, data="1")
列2 (name): 05 416c696365 → "Alice" (长度编码: len=5, data="Alice")
列3 (email): 11 616c69636540... → "[email protected]" (长度编码)
Row 2:
列1 (id): 01 32 → "2"
列2 (name): FB → NULL (0xFB 标识)
列3 (email): 0d 626f6240... → "[email protected]"
Python 解析实现
"""
mysql_text_resultset.py
解析文本结果集(完整的 COM_QUERY 响应)
"""
import struct
from dataclasses import dataclass
from typing import List, Any
def read_length_encoded_int(data: bytes, offset: int) -> tuple:
first = data[offset]
if first == 0xFB:
return None, offset + 1 # NULL
elif first < 0xFB:
return first, offset + 1
elif first == 0xFC:
return struct.unpack('<H', data[offset+1:offset+3])[0], offset + 3
elif first == 0xFD:
return struct.unpack('<I', data[offset+1:offset+4] + b'\x00')[0], offset + 4
elif first == 0xFE:
return struct.unpack('<Q', data[offset+1:offset+9])[0], offset + 9
def read_length_encoded_str(data: bytes, offset: int) -> tuple:
"""读取长度编码字符串,返回 (str|None, new_offset)"""
length, offset = read_length_encoded_int(data, offset)
if length is None:
return None, offset # NULL 值
value = data[offset:offset + length].decode('utf-8', errors='replace')
return value, offset + length
@dataclass
class ColumnDef:
name: str
type_id: int
flags: int = 0
decimals: int = 0
@dataclass
class ResultSet:
columns: List[ColumnDef]
rows: List[List[Any]]
status_flags: int = 0
warnings: int = 0
def parse_text_resultset(packets: List[bytes]) -> ResultSet:
"""
解析一整个文本结果集
参数:
packets: 按顺序的数据包 payload 列表
返回:
ResultSet 对象
"""
idx = 0
result = ResultSet(columns=[], rows=[])
# 1. 解析列数 (长度编码整数)
column_count, _ = read_length_encoded_int(packets[idx], 0)
idx += 1
# 2. 解析列定义
for i in range(column_count):
payload = packets[idx]
col = parse_column_def(payload)
result.columns.append(col)
idx += 1
# 3. EOF 或 OK(列定义结束)
eof1 = packets[idx]
idx += 1
if eof1[0] == 0xFE and len(eof1) >= 5:
result.status_flags = struct.unpack('<H', eof1[3:5])[0]
result.warnings = struct.unpack('<H', eof1[1:3])[0]
# 4. 解析行数据(直到遇到 EOF/OK)
while idx < len(packets):
payload = packets[idx]
idx += 1
# 检查是否是 EOF/OK
if payload[0] == 0xFE and len(payload) < 9:
if len(payload) >= 5:
result.status_flags = struct.unpack('<H', payload[3:5])[0]
break
if payload[0] == 0x00 and len(payload) >= 7:
# OK with CLIENT_DEPRECATE_EOF
break
# 解析行数据
row = parse_row_data(payload, len(result.columns))
result.rows.append(row)
return result
def parse_column_def(payload: bytes) -> ColumnDef:
"""解析单个列定义"""
offset = 0
# catalog
_, offset = read_length_encoded_str(payload, offset)
# schema
_, offset = read_length_encoded_str(payload, offset)
# table alias
_, offset = read_length_encoded_str(payload, offset)
# table name
_, offset = read_length_encoded_str(payload, offset)
# column alias
name, offset = read_length_encoded_str(payload, offset)
# column name
_, offset = read_length_encoded_str(payload, offset)
# fixed-length fields
offset += 1 # 0x0C
_ = struct.unpack('<H', payload[offset:offset+2])[0] # charset
offset += 2
_ = struct.unpack('<I', payload[offset:offset+4])[0] # column_length
offset += 4
type_id = payload[offset]
offset += 1
flags = struct.unpack('<H', payload[offset:offset+2])[0]
offset += 2
decimals = payload[offset]
return ColumnDef(name=name or "", type_id=type_id, flags=flags, decimals=decimals)
def parse_row_data(payload: bytes, num_columns: int) -> List[Any]:
"""解析行数据"""
row = []
offset = 0
for _ in range(num_columns):
value, offset = read_length_encoded_str(payload, offset)
row.append(value)
return row
def format_table(result: ResultSet) -> str:
"""将结果集格式化为 ASCII 表格"""
if not result.columns:
return "(空结果集)"
# 计算列宽
col_widths = []
for col in result.columns:
width = len(col.name)
col_widths.append(width)
for row in result.rows:
for i, val in enumerate(row):
val_str = str(val) if val is not None else "NULL"
col_widths[i] = max(col_widths[i], len(val_str))
# 格式化
lines = []
# 表头
header = " | ".join(col.name.ljust(col_widths[i]) for i, col in enumerate(result.columns))
lines.append(header)
lines.append("-+-".join("-" * w for w in col_widths))
# 数据行
for row in result.rows:
cells = []
for i, val in enumerate(row):
val_str = str(val) if val is not None else "NULL"
cells.append(val_str.ljust(col_widths[i]))
lines.append(" | ".join(cells))
# 底部信息
lines.append("")
lines.append(f"({len(result.rows)} rows, status=0x{result.status_flags:04X})")
return "\n".join(lines)
def demo():
"""演示文本结果集解析"""
print("=" * 70)
print("MySQL 文本结果集解析演示")
print("=" * 70)
# 构造模拟数据包
packets = []
# 列数 = 3
packets.append(b'\x03')
# 列定义 (简化编码)
def make_col_def(name, type_id, charset=0x2D, col_len=255, flags=0, dec=0):
payload = b''
# catalog
payload += b'\x03def'
# schema
payload += b'\x04test'
# table alias
payload += b'\x05users'
# table name
payload += b'\x05users'
# column alias
payload += bytes([len(name)]) + name.encode()
# column name
payload += bytes([len(name)]) + name.encode()
# fixed fields length
payload += b'\x0c'
# charset
payload += struct.pack('<H', charset)
# column_length
payload += struct.pack('<I', col_len)
# type
payload += struct.pack('B', type_id)
# flags
payload += struct.pack('<H', flags)
# decimals
payload += struct.pack('B', dec)
# filler
payload += b'\x00\x00'
return payload
packets.append(make_col_def('id', 0x03, charset=0x3F, col_len=11, flags=0x0003))
packets.append(make_col_def('name', 0xFD, charset=0x2D, col_len=255))
packets.append(make_col_def('age', 0x03, charset=0x3F, col_len=11, flags=0x0020))
# EOF (列定义结束)
packets.append(b'\xfe\x00\x00\x02\x00')
# 行数据
def make_row(*values):
payload = b''
for v in values:
if v is None:
payload += b'\xFB'
else:
s = str(v).encode('utf-8')
payload += bytes([len(s)]) + s
return payload
packets.append(make_row(1, "Alice", 28))
packets.append(make_row(2, "Bob", 35))
packets.append(make_row(3, None, 22))
# EOF (行数据结束)
packets.append(b'\xfe\x00\x00\x02\x00')
# 解析
result = parse_text_resultset(packets)
print(f"\n列数: {len(result.columns)}")
for i, col in enumerate(result.columns):
print(f" 列 {i}: {col.name} (type=0x{col.type_id:02X})")
print(f"\n{format_table(result)}")
if __name__ == '__main__':
demo()
6.7 OK 包详解
OK 包用于表示成功执行(DML 操作、SET 语句等)。
格式
偏移量 大小 字段 说明
──────────────────────────────────────────────────────
0 1 字节 0x00 OK 标识
1 变长 affected_rows 影响的行数 (长度编码整数)
变长 last_insert_id 最后插入的 ID (长度编码整数)
2 字节 status_flags 服务器状态标志
2 字节 warnings 警告数
变长 info 附加信息 (可选, 长度编码字符串)
变长 session_state_changes 会话状态变化 (可选)
状态标志
| 标志 | 值 | 说明 |
|---|---|---|
SERVER_STATUS_IN_TRANS | 0x0001 | 事务进行中 |
SERVER_STATUS_AUTOCOMMIT | 0x0002 | 自动提交开启 |
SERVER_MORE_RESULTS_EXISTS | 0x0008 | 还有更多结果集 |
SERVER_STATUS_NO_GOOD_INDEX_USED | 0x0010 | 未使用好索引 |
SERVER_STATUS_NO_INDEX_USED | 0x0020 | 未使用索引 |
SERVER_STATUS_CURSOR_EXISTS | 0x0040 | 游标打开 |
SERVER_STATUS_LAST_ROW_SENT | 0x0080 | 最后一行已发送 |
SERVER_STATUS_DB_DROPPED | 0x0100 | 数据库被删除 |
SERVER_STATUS_NO_BACKSLASH_ESCAPES | 0x0200 | 禁用反斜杠转义 |
SERVER_STATUS_METADATA_CHANGED | 0x0400 | 元数据已变更 |
SERVER_QUERY_WAS_SLOW | 0x0800 | 查询较慢 |
SERVER_PS_OUT_PARAMS | 0x1000 | 存在 OUT 参数 |
SERVER_STATUS_IN_TRANS_READONLY | 0x2000 | 只读事务 |
SERVER_SESSION_STATE_CHANGED | 0x4000 | 会话状态已变更 |
def parse_ok_packet(payload: bytes) -> dict:
"""解析 OK 包"""
offset = 1 # 跳过 0x00
affected_rows, offset = read_length_encoded_int(payload, offset)
last_insert_id, offset = read_length_encoded_int(payload, offset)
status_flags = struct.unpack('<H', payload[offset:offset+2])[0]
offset += 2
warnings = struct.unpack('<H', payload[offset:offset+2])[0]
offset += 2
info = ""
if offset < len(payload):
info, _ = read_length_encoded_str(payload, offset)
return {
'affected_rows': affected_rows,
'last_insert_id': last_insert_id,
'status_flags': status_flags,
'warnings': warnings,
'info': info,
}
6.8 ERR 包详解
ERR 包用于表示操作失败。
格式
偏移量 大小 字段 说明
──────────────────────────────────────────
0 1 字节 0xFF ERR 标识
1 2 字节 error_code 错误代码
3 1 字节 '#' (0x23) SQL state 标记
4 5 字节 sql_state SQL 状态码
9 变长 error_message 错误消息
常见错误码
| 错误码 | 名称 | 说明 |
|---|---|---|
| 1045 | ER_ACCESS_DENIED_ERROR | 访问被拒绝 |
| 1049 | ER_BAD_DB | 未知数据库 |
| 1050 | ER_TABLE_EXISTS_ERROR | 表已存在 |
| 1054 | ER_BAD_FIELD_ERROR | 未知字段 |
| 1062 | ER_DUP_ENTRY | 唯一键冲突 |
| 1064 | ER_PARSE_ERROR | SQL 语法错误 |
| 1146 | ER_NO_SUCH_TABLE | 表不存在 |
| 1213 | ER_LOCK_DEADLOCK | 死锁 |
| 1406 | ER_DATA_TOO_LONG | 数据过长 |
| 2006 | ER_SERVER_GONE_ERROR | 服务器连接已断开 |
| 2013 | ER_SERVER_LOST | 查询期间丢失连接 |
def parse_err_packet(payload: bytes) -> dict:
"""解析 ERR 包"""
error_code = struct.unpack('<H', payload[1:3])[0]
sql_state = ""
message = ""
if len(payload) > 3 and payload[3:4] == b'#':
sql_state = payload[4:9].decode('ascii', errors='replace')
message = payload[9:].decode('utf-8', errors='replace')
elif len(payload) > 3:
message = payload[3:].decode('utf-8', errors='replace')
return {
'error_code': error_code,
'sql_state': sql_state,
'message': message,
}
6.9 多结果集
当执行多条语句(CLIENT_MULTI_STATEMENTS)或存储过程时,可能返回多个结果集。服务器通过 SERVER_MORE_RESULTS_EXISTS 状态标志通知客户端还有更多结果。
结果集 1:
Column Count → Column Defs → EOF → Row Data → EOF (status: SERVER_MORE_RESULTS_EXISTS)
结果集 2:
Column Count → Column Defs → EOF → Row Data → EOF (status: 0)
def read_all_resultsets(sock, reader):
"""读取所有结果集"""
results = []
while True:
result = parse_text_resultset(reader.read_until_eof())
results.append(result)
if not (result.status_flags & 0x0008): # SERVER_MORE_RESULTS_EXISTS
break
return results
6.10 注意事项
重要提醒
所有值都是字符串:在文本协议中,数值、日期等所有类型的值都以字符串形式传输。客户端需要自行转换类型。
NULL 的表示:NULL 值通过特殊的长度编码
0xFB表示,而不是空字符串。字符集问题:字符串值使用连接协商的字符集编码。如果表的字符集与连接字符集不同,服务器会自动转换。
大结果集的内存:不要一次性将大结果集读入内存,应该使用流式读取。
EOF 与 OK 的区别:当客户端设置了
CLIENT_DEPRECATE_EOF,EOF 包被 OK 包替代。SERVER_MORE_RESULTS_EXISTS:处理多结果集时必须检查此标志,否则会遗漏后续结果。
6.11 业务场景
场景一:ORM 框架的类型映射
ORM(如 SQLAlchemy、Hibernate)收到文本结果集后,需要根据 Column Definition 中的类型信息将字符串值转换为 Python/Java 原生类型。
场景二:查询结果的序列化
某些场景需要将查询结果序列化为 JSON 或 CSV。文本协议天然支持这种转换,因为值已经是字符串格式。
场景三:数据迁移工具
数据迁移工具(如 mysqldump、gh-ost)需要解析文本结果集来获取数据,然后在目标端重新构造 INSERT 语句。
6.12 扩展阅读
上一章:05 - 命令与请求 下一章:07 - 二进制协议 —— 深入理解预处理语句的二进制结果集格式。