强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

MySQL 传输协议精讲 / 06 - 文本协议

第 06 章:文本协议

6.1 文本协议概述

**文本协议(Text Protocol)**是 MySQL 最基础的结果集传输格式。当客户端通过 COM_QUERY 执行 SQL 语句时,服务器使用文本协议返回结果。

文本协议 vs 二进制协议

特性文本协议二进制协议
使用场景COM_QUERYCOM_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", "alice@example.com"
  [Packet 7]  Row Data: 2, "Bob", "bob@example.com"
  [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名称说明
0x00MYSQL_TYPE_DECIMAL定点数
0x01MYSQL_TYPE_TINYINTTINYINT
0x02MYSQL_TYPE_SMALLINTSMALLINT
0x03MYSQL_TYPE_INTINT
0x04MYSQL_TYPE_FLOATFLOAT
0x05MYSQL_TYPE_DOUBLEDOUBLE
0x06MYSQL_TYPE_NULLNULL
0x07MYSQL_TYPE_TIMESTAMPTIMESTAMP
0x08MYSQL_TYPE_BIGINTBIGINT
0x09MYSQL_TYPE_MEDIUMINTMEDIUMINT
0x0AMYSQL_TYPE_DATEDATE
0x0BMYSQL_TYPE_TIMETIME
0x0CMYSQL_TYPE_DATETIMEDATETIME
0x0DMYSQL_TYPE_YEARYEAR
0x0FMYSQL_TYPE_VARCHARVARCHAR
0x10MYSQL_TYPE_BITBIT
0xF5MYSQL_TYPE_JSONJSON
0xF6MYSQL_TYPE_NEWDECIMALDECIMAL (新)
0xF7MYSQL_TYPE_ENUMENUM
0xF8MYSQL_TYPE_SETSET
0xF9MYSQL_TYPE_TINY_BLOBTINYBLOB/TINYTEXT
0xFAMYSQL_TYPE_MEDIUM_BLOBMEDIUMBLOB/MEDIUMTEXT
0xFBMYSQL_TYPE_LONG_BLOBLONGBLOB/LONGTEXT
0xFCMYSQL_TYPE_BLOBBLOB/TEXT
0xFDMYSQL_TYPE_VAR_STRINGVARCHAR/VARBINARY
0xFEMYSQL_TYPE_STRINGCHAR/BINARY
0xFFMYSQL_TYPE_GEOMETRY空间数据

列标志(Flags)

标志说明
NOT_NULL_FLAG0x0001NOT NULL
PRI_KEY_FLAG0x0002主键
UNIQUE_KEY_FLAG0x0004唯一键
MULTIPLE_KEY_FLAG0x0008非唯一索引
BLOB_FLAG0x0010BLOB/TEXT
UNSIGNED_FLAG0x0020UNSIGNED
ZEROFILL_FLAG0x0040ZEROFILL
BINARY_FLAG0x0080BINARY
ENUM_FLAG0x0100ENUM
AUTO_INCREMENT_FLAG0x0200AUTO_INCREMENT
TIMESTAMP_FLAG0x0400TIMESTAMP
SET_FLAG0x0800SET
NO_DEFAULT_VALUE_FLAG0x1000无默认值
ON_UPDATE_NOW_FLAG0x2000ON 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]

每个列值使用长度编码格式:

编码含义
0xFBNULL 值
其他长度编码字符串值(即使数值类型也以文本表示)

示例

-- 查询结果
SELECT id, name, email FROM users LIMIT 2;
-- 结果:
-- | id | name  | email             |
-- |  1 | Alice | alice@example.com |
-- |  2 | NULL  | bob@example.com   |
Row 1:
  列1 (id):    01 31               → "1"     (长度编码: len=1, data="1")
  列2 (name):  05 416c696365       → "Alice" (长度编码: len=5, data="Alice")
  列3 (email): 11 616c69636540...  → "alice@example.com" (长度编码)

Row 2:
  列1 (id):    01 32               → "2"
  列2 (name):  FB                  → NULL    (0xFB 标识)
  列3 (email): 0d 626f6240...      → "bob@example.com"

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_TRANS0x0001事务进行中
SERVER_STATUS_AUTOCOMMIT0x0002自动提交开启
SERVER_MORE_RESULTS_EXISTS0x0008还有更多结果集
SERVER_STATUS_NO_GOOD_INDEX_USED0x0010未使用好索引
SERVER_STATUS_NO_INDEX_USED0x0020未使用索引
SERVER_STATUS_CURSOR_EXISTS0x0040游标打开
SERVER_STATUS_LAST_ROW_SENT0x0080最后一行已发送
SERVER_STATUS_DB_DROPPED0x0100数据库被删除
SERVER_STATUS_NO_BACKSLASH_ESCAPES0x0200禁用反斜杠转义
SERVER_STATUS_METADATA_CHANGED0x0400元数据已变更
SERVER_QUERY_WAS_SLOW0x0800查询较慢
SERVER_PS_OUT_PARAMS0x1000存在 OUT 参数
SERVER_STATUS_IN_TRANS_READONLY0x2000只读事务
SERVER_SESSION_STATE_CHANGED0x4000会话状态已变更
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    错误消息

常见错误码

错误码名称说明
1045ER_ACCESS_DENIED_ERROR访问被拒绝
1049ER_BAD_DB未知数据库
1050ER_TABLE_EXISTS_ERROR表已存在
1054ER_BAD_FIELD_ERROR未知字段
1062ER_DUP_ENTRY唯一键冲突
1064ER_PARSE_ERRORSQL 语法错误
1146ER_NO_SUCH_TABLE表不存在
1213ER_LOCK_DEADLOCK死锁
1406ER_DATA_TOO_LONG数据过长
2006ER_SERVER_GONE_ERROR服务器连接已断开
2013ER_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 注意事项

重要提醒

  1. 所有值都是字符串:在文本协议中,数值、日期等所有类型的值都以字符串形式传输。客户端需要自行转换类型。

  2. NULL 的表示:NULL 值通过特殊的长度编码 0xFB 表示,而不是空字符串。

  3. 字符集问题:字符串值使用连接协商的字符集编码。如果表的字符集与连接字符集不同,服务器会自动转换。

  4. 大结果集的内存:不要一次性将大结果集读入内存,应该使用流式读取。

  5. EOF 与 OK 的区别:当客户端设置了 CLIENT_DEPRECATE_EOF,EOF 包被 OK 包替代。

  6. SERVER_MORE_RESULTS_EXISTS:处理多结果集时必须检查此标志,否则会遗漏后续结果。


6.11 业务场景

场景一:ORM 框架的类型映射

ORM(如 SQLAlchemy、Hibernate)收到文本结果集后,需要根据 Column Definition 中的类型信息将字符串值转换为 Python/Java 原生类型。

场景二:查询结果的序列化

某些场景需要将查询结果序列化为 JSON 或 CSV。文本协议天然支持这种转换,因为值已经是字符串格式。

场景三:数据迁移工具

数据迁移工具(如 mysqldump、gh-ost)需要解析文本结果集来获取数据,然后在目标端重新构造 INSERT 语句。


6.12 扩展阅读


上一章05 - 命令与请求 下一章07 - 二进制协议 —— 深入理解预处理语句的二进制结果集格式。