
本文详解如何将 MySQL 中按日期存储的员工排班数据(emp_id/date/shift),动态转换为「以员工为行、以当月每日为列」的横向表格,并正确填充对应班次,缺失日期显示为“0”。
本文详解如何将 MySQL 中按日期存储的员工排班数据(emp_id/date/shift),动态转换为「以员工为行、以当月每日为列」的横向表格,并正确填充对应班次,缺失日期显示为“0”。
在实际考勤或排班系统中,原始数据常以“长格式”(每条记录含 emp_id、date、shift)存储,但业务展示往往需要“宽格式”——即一行代表一名员工,一列代表一天,便于横向浏览整月排班。直接使用嵌套循环(如原代码中对每个员工遍历所有日期再比对)虽能实现,但存在严重性能与逻辑缺陷:同一员工多条记录会被重复输出多行,且无法保证日期列对齐、空缺填充准确。
✅ 正确方案应分三步实现:
- 预生成当月完整日期数组(确保列头连续、无遗漏)
-
查询并重组数据为关联数组(以
emp_id → date → shift结构缓存) - 双层遍历渲染表格(外层遍历员工,内层遍历日期列,查表填充)
以下是优化后的完整 PHP 教程实现(兼容 MySQLi 面向过程风格,已规避 SQL 注入与逻辑错误):
立即学习“PHP免费学习笔记(深入)”;
<?php
// 1. 生成当月所有日期(格式:Y-m-d)
$year = date('Y');
$month = date('m');
$daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year);
$dates = [];
for ($d = 1; $d <= $daysInMonth; $d++) {
$dateStr = sprintf('%04d-%02d-%02d', $year, $month, $d);
$dates[] = $dateStr;
}
// 2. 连接数据库并获取全量排班数据
$link = mysqli_connect('localhost', 'root', '', 'rostertest');
if (!$link) die("数据库连接失败: " . mysqli_connect_error());
// 查询所有记录,并按 emp_id 分组索引(关键优化!)
$sql = "SELECT emp_id, `date`, `shift` FROM ros WHERE YEAR(`date`) = ? AND MONTH(`date`) = ?";
$stmt = mysqli_prepare($link, $sql);
mysqli_stmt_bind_param($stmt, 'ii', $year, $month);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
// 构建二维关联数组:$data[emp_id][date] = shift
$data = [];
while ($row = mysqli_fetch_assoc($result)) {
$empId = $row['emp_id'];
$date = $row['date'];
$data[$empId][$date] = $row['shift'];
}
mysqli_stmt_close($stmt);
// 3. 获取唯一员工 ID 列表(去重并排序)
$employees = array_keys($data);
sort($employees);
?>
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<th>员工编号</th>
<?php foreach ($dates as $date): ?>
<th><?= date('m/d D', strtotime($date)) ?></th>
<?php endforeach; ?>
</tr>
</thead>
<tbody>
<?php foreach ($employees as $empId): ?>
<tr>
<td><strong><?= htmlspecialchars($empId) ?></strong></td>
<?php foreach ($dates as $date): ?>
<td>
<?= isset($data[$empId][$date])
? htmlspecialchars($data[$empId][$date])
: '0' ?>
</td>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>? 关键改进说明:
- ✅ 避免 N+1 查询:原答案建议的子查询(
SELECT * FROM emp_date WHERE id=...)会为每个员工发起新查询,严重拖慢性能;本方案仅需 1 次查询 + 内存重组。 - ✅ 精准日期对齐:通过
cal_days_in_month()动态计算当月天数,确保列数准确(如 2 月 28/29 天、4 月 30 天)。 - ✅ 安全防护:使用
mysqli_prepare防止 SQL 注入;htmlspecialchars()防止 XSS 输出。 - ✅ 空值鲁棒处理:
isset($data[$empId][$date])确保未排班日期统一显示'0',而非 PHP 警告。
⚠️ 注意事项:
- 若需支持跨月查询,将
$year/$month改为参数化传入; - 生产环境建议将
$data缓存(如 Redis),避免高频重复计算; - 表格列过多时(如 31 列),可添加水平滚动容器或分页组件提升体验。
此方案兼顾可读性、性能与健壮性,是 PHP 动态行列转换的经典实践模式。



















