一般建議:不要。將圖片存入資料庫會導致:
最佳實務:圖片存檔案系統/物件儲存(S3、GCS、MinIO),資料庫僅存路徑/URL/雜湊值。
若因特殊需求(如嚴格交易一致性、小型封閉系統、遷移困難)必須存入 DB,本文提供完整方案比較。
CREATE TABLE `images` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`filename` VARCHAR(255) NOT NULL,
`mime_type` VARCHAR(100) NOT NULL,
`size` INT UNSIGNED NOT NULL,
`data` LONGBLOB NOT NULL, -- 二進位資料
`checksum` CHAR(64) NOT NULL, -- SHA256 校驗和
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_checksum` (`checksum`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
關鍵點:使用 LONGBLOB(4GB 上限),加上 checksum 可快速去重。
測試環境:PHP 8.2、MySQL 8.0、17KB JPEG 圖片。
| 方法 | 儲存大小 | 讀取解碼 | 優點 | 缺點 |
|---|---|---|---|---|
| PMA_sqlAddslashes (phpMyAdmin 內部函式) | 12.8 KB (最小) | 無需解碼 | 體積最小、相容 phpMyAdmin 匯出 | 需複製 phpMyAdmin 程式碼、非標準 |
| Base64 Encode | 17 KB (+33%) | base64_decode() |
標準化、純文字、可直接嵌入 HTML data URI | 體積膨脹 33%、CPU 額外開銷 |
| Bin2Hex | 25 KB (+100%) | MySQL UNHEX() 或 PHP hex2bin() |
純十六進位、SQL 可直接寫入 0x... |
體積翻倍、效能最差 |
| Prepared Statement + 參數綁定 (推薦) | 12.8 KB (原始) | 無需編碼 | 最安全、效能最好、無體積膨脹、防 SQL 注入 | 需使用 PDO/mysqli 預處理 |
<?php
$dsn = "mysql:host=localhost;dbname=test;charset=utf8mb4";
$pdo = new PDO($dsn, 'root', 'password', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
// 上傳處理
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['image'])) {
$file = $_FILES['image'];
// 基本驗證
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($file['type'], $allowedMimes, true)) {
die("不支援的檔案類型");
}
if ($file['size'] > 5 * 1024 * 1024) { // 5MB 限制
die("檔案過大");
}
if ($file['error'] !== UPLOAD_ERR_OK) {
die("上傳錯誤:" . $file['error']);
}
// 讀取二進位資料
$binaryData = file_get_contents($file['tmp_name']);
$checksum = hash('sha256', $binaryData);
// 檢查去重
$stmt = $pdo->prepare("SELECT id FROM images WHERE checksum = ?");
$stmt->execute([$checksum]);
if ($existing = $stmt->fetch()) {
echo "圖片已存在,ID: {$existing['id']}";
exit;
}
// 寫入資料庫(參數綁定自動處理二進位,無需編碼)
$stmt = $pdo->prepare(
"INSERT INTO images (filename, mime_type, size, data, checksum) VALUES (?, ?, ?, ?, ?)"
);
$stmt->bindParam(1, $file['name'], PDO::PARAM_STR);
$stmt->bindParam(2, $file['type'], PDO::PARAM_STR);
$stmt->bindParam(3, $file['size'], PDO::PARAM_INT);
$stmt->bindParam(4, $binaryData, PDO::PARAM_LOB); // 關鍵:LOB 型別
$stmt->bindParam(5, $checksum, PDO::PARAM_STR);
$stmt->execute();
$id = $pdo->lastInsertId();
echo "上傳成功,ID: $id";
}
// 讀取並輸出圖片
if (isset($_GET['id'])) {
$stmt = $pdo->prepare("SELECT mime_type, data FROM images WHERE id = ?");
$stmt->execute([$_GET['id']]);
$img = $stmt->fetch(PDO::FETCH_ASSOC);
if ($img) {
header("Content-Type: {$img['mime_type']}");
header("Content-Length: " . strlen($img['data']));
header("Cache-Control: public, max-age=31536000");
echo $img['data'];
} else {
http_response_code(404);
echo "Not Found";
}
}
?>
<form action="upload.php" method="post" enctype="multipart/form-data">
<input type="hidden" name="MAX_FILE_SIZE" value="5242880">
<input type="file" name="image" accept="image/jpeg,image/png,image/gif,image/webp" required>
<button type="submit">上傳</button>
</form>
若圖片超過 PHP memory_limit,使用資料流分塊寫入:
// 寫入大型 Blob(需 PDO::ATTR_STRINGIFY_FETCHES => false)
$pdo->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, false);
$stmt = $pdo->prepare("INSERT INTO images (data) VALUES (?)");
$fp = fopen($file['tmp_name'], 'rb');
$stmt->bindParam(1, $fp, PDO::PARAM_LOB);
$stmt->execute();
fclose($fp);
COMPRESS() / UNCOMPRESS(),或應用層用 zstd/zlib 壓縮後再存入。data 欄位移至獨立資料表 image_blobs,主表只存中繼資料,避免全表掃描時讀取大量 Blob。若需相容 phpMyAdmin 匯出/匯入 SQL 檔,可參考其 PMA_sqlAddslashes 邏輯,但強烈建議改用參數綁定,現代資料庫驅動已完美支援二進位參數,無需手動轉義。