最近在做一个内部通用的数据导出模块,需求方说得很轻松:“把这张表导出成 CSV 就行,列名用中文表头。”问题是,他们口中的“这张表”一个月能改三四次结构,今天加一列,明天删一列,后天改个字段长度。如果我把列名写死在代码里,基本就是在给自己挖坑。于是我把“PHP 获取 MySQL 表列名”这个老问题重新翻了出来,索性把几种主流做法、优缺点,以及实际项目里能直接用的封装都整理成一篇,给同样被这种需求折磨过的同学参考。
这篇文章不打算只给一个 SHOW COLUMNS 的示例就完事。我会把四种最常见的取字段方式都过一遍,解释每种方式适合什么场景、有哪些坑,最后给一个可以直接复制使用的通用读取类。无论你是维护老项目(还在用 mysqli),还是新项目已经切到 PDO,都能在里面找到合适的方案。
1. 先搞清楚需求:你要的是字段名,还是字段的“完整档案”
1.1 不同场景对应的信息粒度
“获取表列名”这个说法听起来很具体,但实际工作中,背后的需求差别其实挺大的:
- 只需要一组字段名,用来动态拼
SELECT语句或做表单字段映射,比如把数据库字段直接对应到 HTML 表单的 name 属性。 - 需要“字段名 + 字段类型”,用来决定渲染什么控件:
int用数字输入框,varchar用文本框,text用 textarea,datetime用日期选择器。 - 需要“字段名 + 注释 + 是否可空 + 默认值”,用来生成数据字典、导出 CSV 时做中文表头、或者自动生成模型基类。
这三种需求,对应的取数路径完全不同。只查字段名,SHOW COLUMNS 就够了;要做数据字典,还是得老老实实走 information_schema。我见过不少同学在群里问“怎么用 PHP 获取 mysql 表字段名”,底下一堆人回答 SELECT * FROM table LIMIT 1 然后用 array_keys() 拿字段名,这个方法不是不行,但很多场景下拿不到字段类型、注释、默认值这些关键信息,而且白白查了一次数据。
1.2 四种主流方案横向对比
我先把结论放在前面,后面再逐个展开。下面这张表建议保存下来,以后做技术选型时直接翻出来看:
| 获取方式 | 返回信息 | 是否需要查真实数据 | 能否拿到注释 | 兼容性 | 适用场景 |
|---|---|---|---|---|---|
SHOW COLUMNS / SHOW FULL COLUMNS |
字段名、类型、Null、Key、Default、Extra(FULL 版带注释) | 不需要 | FULL 版支持 | 所有 MySQL / MariaDB | 快速获取字段列表、命令行排查 |
information_schema.COLUMNS |
完整元数据,含注释、字符集、排序规则等 | 不需要 | 支持 | SQL 标准,跨数据库 | 数据字典、代码生成、批量统计 |
mysqli_fetch_field() |
字段名、表名、类型常量等 | 需要一次 SELECT(可用 LIMIT 1) | 不支持 | mysqli 扩展 | 已有查询结果后反拿字段信息 |
PDO::getColumnMeta() |
字段名、表名、native_type 等 | 需要一次 SELECT | 不支持 | 驱动差异较大 | 临时调试、内部小工具 |
选型策略很简单:如果你要的是“字段名列表”这种最轻量的信息,优先 SHOW COLUMNS;如果要做动态表单、数据字典、代码生成这类需要完整元数据的活,直接上 information_schema;如果你已经执行了一条 SELECT,比如刚查完数据准备拼 CSV,那就用 mysqli_fetch_field 或 getColumnMeta 顺手把字段名带上,别再多发一条 SQL。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 最直接的写法:SHOW COLUMNS 与 SHOW FULL COLUMNS
2.1 SHOW COLUMNS 返回的六个字段逐个说
SHOW COLUMNS FROM users; 这种写法应该是所有 PHP 开发者最早接触的取字段方式,它返回六个列:Field、Type、Null、Key、Default、Extra。
Field:列名,也是我们最常取的值。Type:完整的数据类型,比如int(11)、varchar(255)、timestamp。注意这个值带长度,如果只想要类型名int、varchar,需要自己用括号切一下。Null:YES或NO,表示该列是否允许 NULL。Key:索引类型。PRI表示主键,UNI表示唯一索引,MUL表示非唯一索引(可能是普通索引或联合索引的一部分),空字符串表示没有索引。Default:默认值。这里有一个坑:如果某列没有默认值,Default字段是NULL;如果默认值本身就是字符串'NULL',这里返回的也是NULL。所以判断时要注意区分“没有默认值”和“默认值是 NULL 字符串”这两种情况。Extra:附加信息,常见的有auto_increment(自增列)、on update CURRENT_TIMESTAMP(更新时自动刷新时间戳)等。
用 SHOW COLUMNS 时如果想同时拿注释,必须用 SHOW FULL COLUMNS FROM users;。它比普通版多了三列:Collation、Privileges、Comment。其中 Comment 就是你建表时写的列注释,在 MySQL 5.5 以后的版本里基本都维护得挺好。我建议直接用 SHOW FULL COLUMNS,信息量更大,多出来的几列也不至于把网络撑爆。
2.2 mysqli 下的实际代码
老项目还在用 mysqli 的话,写法是这样的:
php复制<?php
$mysqli = new mysqli('127.0.0.1', 'user', 'pass', 'blog');
if ($mysqli->connect_errno) {
die('Connect Error: ' . $mysqli->connect_error);
}
$result = $mysqli->query('SHOW FULL COLUMNS FROM posts');
if (!$result) {
die('Query Error: ' . $mysqli->error);
}
$columns = [];
while ($row = $result->fetch_assoc()) {
$columns[] = $row;
}
print_r($columns);
执行完 SHOW FULL COLUMNS 之后,$columns 里每一行就是一个字段的完整信息。如果你只需要字段名数组,可以这样提取:
php复制$columnNames = array_column($columns, 'Field');
array_column() 从 PHP 5.5 就有了,用来提取二维数组里的某个 key 非常顺手。注意返回的 Field 数组顺序就是表结构的定义顺序,不会乱序,这一点在生成表头时很重要。
2.3 DESCRIBE 和 SHOW COLUMNS 到底是什么关系
很多人搞不清 DESCRIBE、DESC、SHOW COLUMNS 三者的区别。直接说结论:DESCRIBE users; 和 DESC users; 与 SHOW COLUMNS FROM users; 输出完全一致,都是那六个列。我实测过 MySQL 5.7 和 8.0 以及 MariaDB,结果都一样。
DESCRIBE 比 SHOW COLUMNS 多一个用法,就是查看某个具体列的信息:DESCRIBE users email;,只返回 email 这一列的信息。这在想快速确认某个字段的类型时很实用,不用把整张表的字段都查出来。
不过要注意,DESCRIBE 没有“FULL”版本,想看注释还是得用 SHOW FULL COLUMNS 或者 information_schema。
3. 更标准也更强大的 information_schema 方案
3.1 为什么我优先推荐这条路
从“可迁移性”和“信息完整度”两个角度考虑,information_schema.COLUMNS 是最稳的方案。
先说可迁移性。SHOW COLUMNS 是 MySQL 特有的语法,将来如果项目要兼容 PostgreSQL 或 SQLite,这套写法基本作废。而 information_schema 是 SQL 标准里定义的信息目录视图,PostgreSQL、SQL Server、MariaDB 都支持。虽然各家在字段细节上有差异,但核心的 COLUMN_NAME、DATA_TYPE、IS_NULLABLE、COLUMN_DEFAULT 这些字段名是一样的,迁移成本低很多。
再说信息完整度。information_schema.COLUMNS 能拿到比 SHOW FULL COLUMNS 更细的信息,比如:
DATA_TYPE:类型名,不带长度,直接就int、varchar,省去解析int(11)的麻烦。CHARACTER_MAXIMUM_LENGTH:字符串类型最大长度,建表单时用来限制输入长度很合适。ORDINAL_POSITION:列在表中的位置序号,从 1 开始,排序时直接用这个字段。COLUMN_COMMENT:列注释,做数据字典、导出表头时必不可少。EXTRA:自增等附加信息。COLUMN_KEY:主键或索引标志。
3.2 标准查询与字段含义
下面是一条完整的取字段 SQL:
sql复制SELECT
COLUMN_NAME,
DATA_TYPE,
CHARACTER_MAXIMUM_LENGTH,
IS_NULLABLE,
COLUMN_DEFAULT,
COLUMN_KEY,
EXTRA,
COLUMN_COMMENT
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = 'blog'
AND TABLE_NAME = 'posts'
ORDER BY ORDINAL_POSITION;
这里有个非常容易踩的坑:TABLE_SCHEMA 必须写。如果一个 MySQL 实例上同时有多个业务库,不写 TABLE_SCHEMA 的话,information_schema.COLUMNS 会把所有库中同名表的列都返回回来,结果直接翻倍甚至翻好几倍。不要想当然地认为“连接时已经指定了数据库就不用写”。
还有一点,ORDER BY ORDINAL_POSITION 也很重要。虽然大多数情况下 MySQL 返回的行序就是建表顺序,但作为 SQL 标准视图,不应该依赖它的默认返回顺序,显式排序是最稳妥的。做表头、做表单,字段顺序乱了非常难受。
3.3 一次查询拿到多张表的字段名
如果要处理多张表,千万别在循环里一条条查,那会触发 N+1 查询问题。比如一个数据字典工具要扫描整个库 200 张表,循环查就是 200 次网络往返,换成一条 SQL 就能搞定:
sql复制SELECT
TABLE_NAME,
COLUMN_NAME,
ORDINAL_POSITION,
DATA_TYPE,
IS_NULLABLE,
COLUMN_DEFAULT,
COLUMN_KEY,
EXTRA,
COLUMN_COMMENT
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = 'blog'
AND TABLE_NAME IN ('posts', 'users', 'comments')
ORDER BY TABLE_NAME, ORDINAL_POSITION;
PHP 端拿到结果后,用 array_reduce 或循环按 TABLE_NAME 分组:
php复制$grouped = [];
foreach ($rows as $row) {
$grouped[$row['TABLE_NAME']][] = $row;
}
我之前写过一个内部数据库文档工具,就是靠这一条 SQL 把整个库的表结构一次拉全,然后生成 Markdown 格式的数据字典,性能非常好。如果你也要处理多张表,优先考虑这种批量做法。
另外提醒一点:中文注释在 information_schema 中能否正常显示,取决于连接字符集。PHP 连接 MySQL 时如果用的是默认 utf8mb4,一般没问题;但如果是老项目用的 latin1 连接,注释里的中文会变成乱码。建议 PDO 连接串里显式加上 charset=utf8mb4。
4. 从查询结果反推字段名:mysqli_fetch_field 与 PDO getColumnMeta
4.1 mysqli_fetch_field 的用法与几个值得注意的限制
前面两种方式都是“为了拿字段信息而发一条查询”,而 mysqli_fetch_field 则是在已经执行了 SELECT 之后,从结果集元数据里直接反推出字段名。它的好处是省一次查询,缺点也有不少。
php复制<?php
$mysqli = new mysqli('127.0.0.1', 'user', 'pass', 'blog');
$result = $mysqli->query('SELECT * FROM posts LIMIT 1');
if (!$result) {
die('Query Error: ' . $mysqli->error);
}
while ($field = mysqli_fetch_field($result)) {
echo $field->name . PHP_EOL;
// 还可以拿到:
// $field->table 表名
// $field->type 字段类型常量,如 MYSQLI_TYPE_STRING
// $field->max_length 字段值最大长度
}
关键点有三个:
第一,SELECT * 不能改成 SELECT name, email FROM posts 这种指定字段列表的写法。一旦指定了字段列表,fetch_field 只能拿到你列出的那些字段,其他字段信息就丢了。要拿全表字段,就必须用 SELECT *。
第二,LIMIT 1 只是为了避免把整表数据拉到内存,但即使表是空的(0 行数据),结果集依然包含字段描述信息,fetch_field 照样能正常工作。这是很多人不太清楚的一个特性,我在空表上验证过多次。
第三,$field->type 返回的是 PHP 定义的整型常量,比如 MYSQLI_TYPE_STRING、MYSQLI_TYPE_LONG,不能直接当字符串用。如果要做类型判断,需要自己维护一个常量映射表,或者用 $field->flags 来判断是否为自增、主键等属性。这个复杂度比 information_schema 高不少,所以它更适合在“我已经查完数据,顺手拿一下字段名”的场景用,而不是作为获取表结构的主方案。
4.2 PDO getColumnMeta 为什么我说“不要裸用”
PDO 下有一个看起来很方便的方法叫 getColumnMeta(),很多教程里简单提过,但实际用起来坑是真不少。
php复制<?php
$pdo = new PDO('mysql:host=127.0.0.1;dbname=blog;charset=utf8mb4', 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$stmt = $pdo->query('SELECT * FROM posts LIMIT 1');
for ($i = 0; $i < $stmt->columnCount(); $i++) {
$meta = $stmt->getColumnMeta($i);
var_dump($meta);
}
问题在于,PHP 官方文档里明确标注了这个方法是“实验性”的。不同 PDO 驱动返回的数组结构不一样,mysqlnd 驱动下 native_type 有时会返回 VAR_STRING 这种底层存储类型,而不是你建表时写的 varchar。flags 字段在不同版本之间的完整性也差异很大,有的版本能返回主键标志,有的版本就一个空数组。
我在实际项目中踩过一个很深的坑:用 getColumnMeta 判断字段类型来自动生成表单控件,结果同一个字段在开发环境(mysqlnd)和生产环境(另一个 PHP 版本的 mysqlnd)返回的类型标识不一样,导致生产环境把 text 类型渲染成了普通文本框。排查了大半天才发现是 native_type 在不同版本下的差异。
所以我的建议是:getColumnMeta 可以用,但只适合调试脚本、内部小工具这类“坏了也无所谓”的场景。生产环境的核心逻辑,尤其是要根据字段类型做判断、做渲染的地方,老老实实走 information_schema 或 SHOW FULL COLUMNS。
4.3 这一族的“真香场景”到底在哪里
那 fetch_field / getColumnMeta 就一无是处吗?也不是。它最合适的场景是:业务已经执行了一条 SELECT,紧接着需要字段名来拼接下来的逻辑。
举个例子:写一个通用的 CSV 导出函数,接收一个 PDOStatement 对象,把查询结果导出成 CSV。这时用 getColumnMeta 拿表头,就是最优雅的做法,不需要针对每张表额外查一次结构信息:
php复制function exportToCsv(PDOStatement $stmt, string $filename): void
{
$header = [];
for ($i = 0; $i < $stmt->columnCount(); $i++) {
$meta = $stmt->getColumnMeta($i);
$header[] = $meta['name'];
}
$fp = fopen($filename, 'w');
fputcsv($fp, $header);
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
fputcsv($fp, $row);
}
fclose($fp);
}
这种用法,即使字段类型判断有偏差也不影响结果,因为 CSV 里都是字符串输出,不存在类型问题。所以我说,选型要看场景,别拿着一个方法硬套所有需求。
5. 一个可落地的通用 Schema 读取类
5.1 设计思路
把前面几种方案理清之后,我最终在项目里沉淀了一个通用的 Schema 读取类。它的设计思路是这样的:
- 构造函数注入
PDO实例,不关心连接细节。 - 核心方法
getColumns($table)返回该表的完整字段元数据,直接走information_schema,因为信息最全、可迁移性最好。 - 提供
getColumnNames($table)快捷方法,只返回字段名数组,适合动态拼 SQL 的场景。 - 内部做请求级缓存,同一个请求内多次调用同一张表不会重复查库。
- 支持手动指定库名;不指定时自动使用
SELECT DATABASE()获取当前连接库名。 - 表名、库名在拼接时做了严格校验和白名单处理,避免 SQL 注入风险。
这里要特别说明一个技术细节:PDO 的预处理参数只能绑定值,不能绑定表名和字段名。SELECT * FROM ? 这种写法是会直接报错的。所以表名无法走占位符,只能拼接字符串。拼接就意味着必须自己做安全处理,我的做法是先对表名做正则白名单校验,再用反引号包裹,双保险。
5.2 完整代码
php复制<?php
final class MySQLSchemaReader
{
private PDO $pdo;
private string $database;
private array $columnsCache = [];
public function __construct(PDO $pdo, ?string $database = null)
{
$this->pdo = $pdo;
$this->database = $database ?: (string) $pdo->query('SELECT DATABASE()')->fetchColumn();
}
public function getColumns(string $table): array
{
$table = $this->sanitizeIdentifier($table);
if (isset($this->columnsCache[$table])) {
return $this->columnsCache[$table];
}
$sql = "SELECT
COLUMN_NAME,
DATA_TYPE,
CHARACTER_MAXIMUM_LENGTH,
IS_NULLABLE,
COLUMN_DEFAULT,
COLUMN_KEY,
EXTRA,
COLUMN_COMMENT
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = :schema
AND TABLE_NAME = :table
ORDER BY ORDINAL_POSITION";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([
':schema' => $this->database,
':table' => $table,
]);
$columns = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($columns)) {
throw new RuntimeException(
"Table \"{$table}\" not found in database \"{$this->database}\""
);
}
return $this->columnsCache[$table] = $columns;
}
public function getColumnNames(string $table): array
{
return array_column($this->getColumns($table), 'COLUMN_NAME');
}
public function getColumnComments(string $table): array
{
$result = [];
foreach ($this->getColumns($table) as $column) {
if ($column['COLUMN_COMMENT'] !== '') {
$result[$column['COLUMN_NAME']] = $column['COLUMN_COMMENT'];
}
}
return $result;
}
public function getAllTablesColumns(array $tables): array
{
if (empty($tables)) {
return [];
}
$placeholders = [];
$params = [':schema' => $this->database];
foreach (array_values($tables) as $index => $table) {
$safeTable = $this->sanitizeIdentifier($table);
$key = ':table' . $index;
$placeholders[] = $key;
$params[$key] = $safeTable;
}
$inPlaceholders = implode(', ', $placeholders);
$sql = "SELECT
TABLE_NAME,
COLUMN_NAME,
ORDINAL_POSITION,
DATA_TYPE,
IS_NULLABLE,
COLUMN_DEFAULT,
COLUMN_KEY,
EXTRA,
COLUMN_COMMENT
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = :schema
AND TABLE_NAME IN ({$inPlaceholders})
ORDER BY TABLE_NAME, ORDINAL_POSITION";
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
$grouped = [];
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$grouped[$row['TABLE_NAME']][] = $row;
}
return $grouped;
}
private function sanitizeIdentifier(string $identifier): string
{
if (!preg_match('/^[A-Za-z0-9_]+$/', $identifier)) {
throw new InvalidArgumentException(
"Invalid identifier: {$identifier}"
);
}
return $identifier;
}
}
简单说几个实现上的细节:
sanitizeIdentifier()用正则把表名限制在字母、数字、下划线范围内。如果你项目里有特殊字符的表名,比如带连字符的,需要额外扩展这个白名单,但正常情况下不建议表名里有特殊字符。getAllTablesColumns()里的IN子句是动态生成的,占位符用:table0、:table1这种形式。这里同样做了白名单校验,因为表名不能参数绑定。COLUMN_DEFAULT在 MySQL 8.0 中对于timestamp类型有额外细节,但读取出来一般是字符串或 NULL,不需要额外处理。- 构造函数里如果 PDO 连接时没有指定库名,比如 DSN 里没写
dbname,SELECT DATABASE()会返回 NULL。所以这个类支持第二个参数手动指定库名,这也是我建议保持的灵活性。
5.3 用这个类快速实现动态表单和导出
有了这个类之后,动态表头、动态表单这些功能实现起来就很简单了。
比如做 CSV 导出,表头用字段注释,数据用字段名:
php复制$schema = new MySQLSchemaReader($pdo);
$table = 'products';
$columns = $schema->getColumns($table);
$aliasMap = [];
foreach ($columns as $col) {
$aliasMap[$col['COLUMN_NAME']] = $col['COLUMN_COMMENT'] ?: $col['COLUMN_NAME'];
}
// 组装 SELECT 语句
$selectCols = array_map(
fn(string $name) => "`{$name}`",
array_keys($aliasMap)
);
$sql = 'SELECT ' . implode(', ', $selectCols) . ' FROM `' . $table . '`';
$stmt = $pdo->query($sql);
$fp = fopen('export.csv', 'w');
// 写入中文表头
fputcsv($fp, array_values($aliasMap));
// 写入数据
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
fputcsv($fp, $row);
}
fclose($fp);
动态表单生成也类似:拿到 DATA_TYPE 和 IS_NULLABLE 就能决定控件类型和是否必填,COLUMN_COMMENT 作为 label,CHARACTER_MAXIMUM_LENGTH 作为 maxlength。这一套逻辑我在一个后台管理系统的生成器里用得很顺手,新表上线只需要跑一遍生成器,CRUD 页面就出来了。
6. 实战中的坑与性能优化建议
6.1 表名大小写与多库连接
先说说大小写。MySQL 在 Linux 上默认 lower_case_table_names=0,表名是区分大小写的;Windows 和 macOS 上默认 1,不区分大小写。开发环境在 Windows 上写着 SHOW COLUMNS FROM Users 跑得好好的,部署到 Linux 服务器上就报“表不存在”,这是很经典的环境差异问题。
解决方案是统一规范:所有表名、字段名一律小写加下划线,避免在代码里写大小写混合的表名。如果历史表名已经有大写,部署时把 lower_case_table_names=1 也配到 MySQL 配置里,但改这个参数需要重启 MySQL,而且会影响已有数据的大小写处理,新项目建议从一开始就统一小写。
再就是多库连接的问题。如果 PDO 连接串里写了 dbname=blog,然后又执行了 USE another_db,此时 SHOW COLUMNS FROM posts 查的是 another_db 里的 posts 表,而不是连接时指定的那个库。information_schema 方案没有这个问题,因为 TABLE_SCHEMA 是显式指定的。这也是我在通用类里坚持用 information_schema 的原因之一。
6.2 权限问题导致的“查不到字段”
这是一个比较隐蔽的坑。MySQL 对 information_schema 的可见性是基于当前用户权限的,一个用户只能看到自己有权访问的表。如果你给某个只读用户只授了部分表的 SELECT 权限,那么它查 information_schema.COLUMNS 时,那些没权限的表根本不会出现。此时会出现一个诡异现象:表能正常查询(因为就那一张表有权限),但查 information_schema 却找不到这张表。
如果你在排查“为什么查 information_schema 返回空”的问题,先用这条命令看看当前用户的权限:
sql复制SHOW GRANTS FOR CURRENT_USER;
另外,SHOW FULL COLUMNS 也有权限限制,至少要对该表有 SELECT 权限才能看到字段信息。如果连 SELECT 权限都没有,SHOW COLUMNS 直接会报错。
6.3 缓存策略:别每次都查一遍 information_schema
表结构在一个请求的生命周期内几乎不可能发生变化,所以缓存是非常必要的。我常用的有三层缓存策略,按需组合:
- 请求级缓存:类属性
$columnsCache就够了,一个 PHP 请求内重复调用同一张表只查一次库。这是必须做的,零成本。 - 应用级缓存(APCu):如果同一个 PHP-FPM 工作进程内多次请求都要用同一张表的结构,可以用
apcu_store('table_cols_' . $table, $columns, 300)缓存 300 秒。表结构变更后需要手动清缓存,或者等待过期。 - 文件缓存:适合没有 APCu 扩展的环境,把
serialize()后的数组写入临时目录,缓存时间自己控制。
我个人最推荐的是“请求级缓存 + APCu”组合。注意一点:如果你们团队有 DBA 会在生产环境直接改表结构,改完一定要同步清掉 APCu 里的表结构缓存,否则新加的字段要等缓存过期才能生效,那个“为什么我加了字段代码不认”的疑惑就是这么来的。
6.4 拼接 SQL 时:反引号、关键字与注入防御
最后聊聊 SQL 拼接的细节。表名和字段名无法用 PDO 占位符绑定,只能拼接。拼接时有两个细节:
第一,字段名要用反引号包起来。MySQL 的关键字很多,比如 order、key、group、desc 这些,虽然不是全部保留字,但作为字段名时很容易出问题。用反引号包裹之后,即使字段名叫 order 也能正常查询。反引号内部的转义规则是:字段名里如果包含反引号,把反引号替换成两个反引号。不过只要你按 [A-Za-z0-9_] 白名单校验,就根本不会出现这种情况。
第二,永远不要直接把外部参数当表名拼进 SQL。比如从 URL 参数里拿表名:<?php $table = $_GET['table']; $sql = "SELECT * FROM {$table}"; ?>,这种代码等于给攻击者开了一扇门。虽然表名不能参数绑定,但可以先白名单校验,再拼接:
php复制if (!preg_match('/^[A-Za-z0-9_]+$/', $table)) {
throw new InvalidArgumentException('Invalid table name');
}
$sql = "SELECT * FROM `{$table}`";
这是我所有通用工具类的底线,千万不能省。
6.5 老项目迁移:mysql_list_fields 已成历史
排查老项目时还会碰到一种情况:代码里用了 mysql_list_fields() 或 mysql_num_fields() 这种上古函数。这些是 PHP 5 时代 mysql 扩展的函数,PHP 7.0 移除 mysql 扩展后就彻底没了,运行会直接报致命错误。
这类代码在迁移到 PHP 7/8 时,直接替换成 SHOW COLUMNS 或 information_schema 查询即可。具体的映射关系是:mysql_list_fields($db, $table, $conn) 对应 SHOW COLUMNS,mysql_num_fields() 对应 mysqli_num_fields() 或 PDOStatement::columnCount()。
最后再分享一个我的习惯:凡是做动态表头、数据字典、代码生成器这类通用能力,我优先使用 information_schema 方案,因为它信息最全、可迁移性强;而 SHOW COLUMNS 更多是在命令行快速排查时用。至于 getColumnMeta,请把它当成“调试友好”的工具,而不是生产逻辑的地基。在实际项目中,上面这套 MySQLSchemaReader 类已经帮我处理过上百张表的动态导出任务,表结构再怎么变,代码一行都不用动。这个方向的项目,走这条路是最省心的。
