PDO 的 fetchAll() 方法在默认情况下会把结果集中的每一行包装成关联数组,外层再用数字索引包一层。这种结构适合表格渲染,但很多数据接口只需要一个干净的一维数组。要拿到扁平化索引数组,关键不是查询后二次循环,而是理解 PDO 的提取模式。接下来从默认返回结构说起,逐步覆盖单列提取、键值对构建和可复用封装。

PDO 默认返回结构的嵌套来源
调用 fetchAll() 而不传任何参数时,PDO 会使用 PDO::FETCH_BOTH 模式。它会为每一行同时生成数字索引和关联字段两种键,比如 0 => 1 与 id => 1 同时存在,而外层又用行号把所有这些行包成一个数组。也就是说,一次简单查询实际返回的是行数组的数组,每行内部还混合了两种键格式。这种设计兼容旧代码,但数据密度偏低。
如果只想保留列名字段,可以使用 PDO::FETCH_ASSOC,输出的仍是二维数组,只是每行少了数字键,内存占用略小。只要目标不是一整张表,而是一个值列表,这层行号索引就会成为多余的嵌套。很多代码因此在 foreach 里手动拼接新数组,看似直观,却增加了 PHP 层面的循环成本。
要消除这层嵌套,可以从查询方式上入手。PDO 本身提供了专门的提取模式,能够直接生成一维数组,不需要在 PHP 侧做二次加工。
用 PDO::FETCH_COLUMN 生成扁平索引数组
当 SQL 只需要返回一列时,最适合的做法是给 fetchAll() 传 PDO::FETCH_COLUMN。它的第二个参数是列序号,从 0 开始。例如查询 SELECT id FROM users ORDER BY id,目标列就在第 0 位,可以这样写。
<?php
$stmt = $pdo->query('SELECT id FROM users ORDER BY id');
$ids = $stmt->fetchAll(PDO::FETCH_COLUMN, 0);
print_r($ids);
?>
得到的结果就是普通的扁平索引数组,形如 [1, 2, 3, 4],没有行号,也没有字段名。注意列序号和 SQL 中 SELECT 后面的字段顺序严格对应。若查询写成 SELECT name, id FROM users,想要 id 列就必须把第二个参数传成 1,传 0 会拿到 name 列。如果不确定字段顺序,最简单的方法是把目标列放到查询列表的第一位,或者使用别名但别依赖别名序号。
与手动 foreach 拼数组相比,PDO::FETCH_COLUMN 直接在 PDO 内部完成提取,PHP 层不需要遍历和添加元素。对于几万行以上的结果集,这样做不仅代码更短,内存分配也更连续。当然,如果结果集很小,差异可以忽略,但养成用原生模式提取的习惯,能减少一次不必要的数组操作。
构建扁平键值对与键重复问题
有时候需要的不是纯索引数组,而是以某一列为键、另一列为值的键值对。例如把用户表的主键 id 映射到 name。PDO 提供了 PDO::FETCH_KEY_PAIR,要求查询结果恰好两列,第一列自动成为键,第二列成为值。
<?php
$stmt = $pdo->query('SELECT id, name FROM users');
$map = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
print_r($map);
?>
这样得到的是一维关联数组,比如 [1 => 'Alice', 2 => 'Bob']。但这里有一个很容易被忽略的问题:如果第一列出现重复值,后面的行会直接覆盖前面的值,过程不会报错也不会提示。也就是说,当查询结果的第一列不是唯一键时,最终数组可能比预期短,且丢失数据。因此在使用 FETCH_KEY_PAIR 前,务必确保 SQL 的第一列在结果集内唯一,可以用主键、去重或 GROUP BY 来保证。
如果最终想要索引数组而不是关联数组,可以先使用 FETCH_KEY_PAIR 再调用 array_values(),但这只在键本就应该丢弃时才有意义。多数需要一维值列表的场景,直接使用 FETCH_COLUMN 更明确,也避免了键重复带来的覆盖风险。
多列结果转扁平数组的封装与边界
对于需要复用查询结果的代码,可以把单列提取封装成函数,内部使用预处理语句避免 SQL 注入,并返回数组。下面的函数接收 PDO 实例、SQL 字符串和目标列序号。
<?php
function fetchFlatColumn(PDO $pdo, string $sql, int $columnIndex = 0): array
{
$stmt = $pdo->prepare($sql);
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_COLUMN, $columnIndex);
$stmt->closeCursor();
return $result;
}
$ids = fetchFlatColumn($pdo, 'SELECT id FROM users WHERE status = 1');
?>
这里的 columnIndex 只允许整数,不能把用户输入直接传进去。如果列位置需要动态变化,建议在代码中通过白名单将业务字段名映射为序号,而不是拼接 SQL 字符串。预处理语句能保护数据参数,但无法保护列名或列序号这类 SQL 结构部分。
执行完查询后调用 closeCursor() 是为了释放游标,让同一个 PDO 连接可以继续执行其他查询。虽然大多数驱动在结果集取完后会自动释放,但在长连接或循环查询场景下,显式关闭更稳妥。还要注意,fetchAll() 在没有匹配行时返回空数组,而不是布尔值 false,所以封装函数可以直接返回数组类型,调用方配合 count() 或 empty() 做空判断即可。
从默认嵌套结构到扁平索引数组,核心思路是让 PDO 在查询层完成形状转换,而不是把结果拿到 PHP 层再加工。掌握 PDO::FETCH_COLUMN 和 PDO::FETCH_KEY_PAIR 这两种模式,可以覆盖大多数一维数组需求,代码也会更清晰。