在第 18 章中,我們已經使用 MQTT 建立 Raspberry Pi IoT 系統,讓 DHT11 / DHT22 的溫度與濕度資料可以即時發布到 MQTT Broker。
例如:
DHT22
│
▼
Raspberry Pi
│
│ MQTT Publish
▼
Mosquitto
│
▼
Subscriber
這種架構可以讓其他裝置知道:
目前溫度:28.6°C
目前濕度:67.2%
但是,新的問題隨之出現。
如果我們想知道:
- 昨天最高溫是多少?
- 今天凌晨的濕度是多少?
- 過去 24 小時溫度變化如何?
- 最近一星期有哪些時間超過 30°C?
- 風扇啟動前的室內溫度是多少?
- 某一天到底記錄了多少筆資料?
只靠 MQTT 即時訊息是不夠的。
我們還需要:
Database 資料庫
因此本章要為 Raspberry Pi 加入:
SQLite
讓 Raspberry Pi 可以把每一次感測器讀值儲存下來,形成真正的:
環境監測歷史資料庫。
本章最後將完成:
Raspberry Pi 溫溼度歷史資料庫
系統架構將變成:
DHT11 / DHT22
│
▼
Raspberry Pi
│
▼
Python
│
├────────► SQLite
│ │
│ ▼
│ History Data
│
└────────► MQTT
│
▼
Real-Time Data
這也是第 20 章「Raspberry Pi 智慧環境監控中心」最後一個重要資料層。
一、本章學習目標
完成本章後,你將了解:
- 為什麼 IoT 系統需要 Database
- Database 基本概念
- SQLite 是什麼
- Table 是什麼
- Row 與 Column
- Primary Key
- SQL 基礎
- CREATE TABLE
- INSERT
- SELECT
- WHERE
- ORDER BY
- LIMIT
- Timestamp
- Python sqlite3
- Parameterized Query
- Sensor Data Logging
- Historical Data Query
- Aggregate Function
- CSV 匯出
- Database Backup 基本概念
並完成:
Raspberry Pi 溫溼度歷史資料庫
二、為什麼 Raspberry Pi IoT 需要資料庫?
假設 DHT22 每 60 秒讀一次資料:
08:00 27.8°C 68%
08:01 27.9°C 68%
08:02 28.0°C 67%
08:03 28.1°C 67%
如果只使用:
print()
資料只會出現在 Terminal。
程式停止之後,先前資料就不容易繼續分析。
如果只透過 MQTT:
home/room1/temperature
發布最新溫度,MQTT 比較偏向:
即時訊息傳輸。
而資料庫負責:
長期資料保存。
因此:
MQTT
=
現在發生什麼
SQLite
=
過去發生什麼
這兩者是非常好的組合。
三、什麼是 Database?
Database 中文稱為:
資料庫。
它的目的就是有系統地儲存資料。
例如 Raspberry Pi 環境監測系統可以保存:
| id | timestamp | temperature | humidity |
|---|---|---|---|
| 1 | 2026-08-18 08:00:00 | 27.8 | 68.0 |
| 2 | 2026-08-18 08:01:00 | 27.9 | 68.0 |
| 3 | 2026-08-18 08:02:00 | 28.0 | 67.5 |
| 4 | 2026-08-18 08:03:00 | 28.1 | 67.2 |
這些資料就可以永久保存在 Raspberry Pi 儲存裝置中。
之後即使:
Python 程式停止
Raspberry Pi 重新開機
MQTT Subscriber 離線
歷史資料仍然存在。
四、資料庫可以做什麼?
有了 Database 之後,可以回答很多問題。
例如:
目前溫度是多少?
可以查最新一筆。
今天最高溫是多少?
可以使用:
MAX()
今天平均溫度是多少?
可以使用:
AVG()
有哪些資料超過 30°C?
可以使用:
WHERE temperature >= 30
最近 20 筆資料?
可以使用:
ORDER BY
LIMIT
這就是 Database 相較於單純文字檔的重要價值。
五、為什麼選擇 SQLite?
Database 有很多種類。
例如:
SQLite
MySQL
MariaDB
PostgreSQL
對 Raspberry Pi 初學專案而言,SQLite 有非常大的優勢。
六、SQLite 的特色
SQLite 是一種:
Embedded Relational Database
它不像 MySQL 一定需要另外執行 Database Server Process。
SQLite 最重要的特色是:
Database
↓
一個檔案
例如:
environment.db
這個檔案本身就是資料庫。
因此整體架構很簡單:
Python
│
▼
sqlite3
│
▼
environment.db
非常適合:
- Raspberry Pi
- IoT
- Sensor Logger
- 小型 Web App
- Edge Device
- Local Data Cache
- 教學專案
七、SQLite 和 MySQL 有什麼不同?
簡單比較:
| 項目 | SQLite | MySQL / MariaDB |
|---|---|---|
| 需要 Database Server | 否 | 是 |
| 安裝複雜度 | 低 | 較高 |
| Database 儲存 | 單一檔案 | Server 管理 |
| 小型 IoT | 很適合 | 適合大型系統 |
| 多使用者高併發 | 較有限 | 較適合 |
| Raspberry Pi 入門 | 非常適合 | 較進階 |
| Python 支援 | 內建 sqlite3 | 需 Driver |
因此本章選用:
SQLite
八、什麼是 Table?
Relational Database 中,資料通常儲存在:
Table
也就是資料表。
我們建立:
environment_data
用來保存環境資料。
例如:
| id | timestamp | temperature | humidity |
|---|---|---|---|
| 1 | 2026-08-18 08:00:00 | 27.8 | 68.0 |
| 2 | 2026-08-18 08:01:00 | 27.9 | 67.8 |
Table 可以理解成一張有固定欄位結構的表格。
九、什麼是 Column?
Column 就是資料欄位。
本章建立:
id
timestamp
temperature
humidity
其中:
id
資料編號。
timestamp
資料產生時間。
temperature
溫度。
humidity
濕度。
十、什麼是 Row?
每一次 Sensor Reading 都是一筆:
Row
例如:
1 | 2026-08-18 08:00:00 | 27.8 | 68.0
下一次:
2 | 2026-08-18 08:01:00 | 27.9 | 67.8
因此:
Column
=
資料欄位
Row
=
一筆完整紀錄
十一、什麼是 Primary Key?
資料表中通常需要有一個可以唯一識別每一筆資料的欄位。
例如:
id
第一筆:
1
第二筆:
2
第三筆:
3
這個欄位就可以設計為:
Primary Key
例如 SQLite:
id INTEGER PRIMARY KEY AUTOINCREMENT
表示 SQLite 自動產生流水號。
十二、什麼是 SQL?
SQL 是:
Structured Query Language
中文通常稱為:
結構化查詢語言。
它用來操作 Relational Database。
常見指令:
CREATE TABLE
INSERT
SELECT
UPDATE
DELETE
本章最主要使用:
CREATE TABLE
INSERT
SELECT
十三、CREATE TABLE
建立資料表:
CREATE TABLE environment_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
temperature REAL,
humidity REAL
);
這代表建立:
environment_data
包含:
id
timestamp
temperature
humidity
四個欄位。
十四、SQLite 常見資料型別
SQLite 常見型別:
| 型別 | 用途 |
|---|---|
| INTEGER | 整數 |
| REAL | 浮點數 |
| TEXT | 文字 |
| BLOB | Binary Data |
| NULL | 空值 |
本章:
id → INTEGER
temperature → REAL
humidity → REAL
timestamp → TEXT
十五、INSERT 是什麼?
INSERT 用來:
新增資料。
例如:
INSERT INTO environment_data
(
timestamp,
temperature,
humidity
)
VALUES
(
'2026-08-18 08:00:00',
28.5,
67.2
);
執行後資料表多一筆:
| id | timestamp | temperature | humidity |
|---|---|---|---|
| 1 | 2026-08-18 08:00:00 | 28.5 | 67.2 |
十六、SELECT 是什麼?
SELECT 用來:
讀取資料。
例如:
SELECT *
FROM environment_data;
代表:
取得 environment_data
所有欄位
所有資料
十七、SELECT 指定欄位
例如只查:
timestamp
temperature
可以:
SELECT
timestamp,
temperature
FROM environment_data;
這樣就不必取得不需要的欄位。
十八、WHERE 是什麼?
如果只想取得:
temperature >= 30
可以:
SELECT *
FROM environment_data
WHERE temperature >= 30;
這樣就能找到:
高溫紀錄。
十九、ORDER BY
如果要按照時間排序:
SELECT *
FROM environment_data
ORDER BY timestamp ASC;
ASC:
由小到大
由早到晚
如果:
ORDER BY timestamp DESC;
就是:
由大到小
由晚到早
因此查最新資料時很常使用:
ORDER BY timestamp DESC
二十、LIMIT
例如:
SELECT *
FROM environment_data
ORDER BY id DESC
LIMIT 10;
代表:
取得最新 10 筆紀錄。
對 Raspberry Pi Web Dashboard 非常實用。
二十一、什麼是 Timestamp?
Sensor Data 只有:
28.5°C
其實不夠。
因為我們不知道:
這個 28.5°C 是什麼時候測量的?
因此需要:
Timestamp
例如:
2026-08-18 08:45:00
完整資料:
Temperature:
28.5°C
Humidity:
67.2%
Timestamp:
2026-08-18 08:45:00
二十二、Python 如何取得時間?
可以使用:
from datetime import datetime
取得現在時間:
now = datetime.now()
格式化:
timestamp = now.strftime(
"%Y-%m-%d %H:%M:%S"
)
結果例如:
2026-08-18 08:45:00
二十三、Python 內建 sqlite3
Python 標準函式庫就提供:
sqlite3
因此通常不需要額外使用:
pip install sqlite3
直接:
import sqlite3
即可操作 SQLite。
這是 Raspberry Pi 使用 SQLite 最大的便利之一。
二十四、建立專案目錄
先建立:
mkdir -p ~/environment_database
進入:
cd ~/environment_database
未來檔案結構:
environment_database/
│
├── environment.db
├── create_database.py
├── insert_test.py
├── query_data.py
├── sensor_logger.py
└── export_csv.py
二十五、建立第一個 SQLite Database
建立:
nano create_database.py
輸入:
import sqlite3
DB_FILE = "environment.db"
connection = sqlite3.connect(
DB_FILE
)
cursor = connection.cursor()
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS environment_data
(
id INTEGER
PRIMARY KEY
AUTOINCREMENT,
timestamp TEXT
NOT NULL,
temperature REAL,
humidity REAL
)
"""
)
connection.commit()
connection.close()
print(
"Database initialized."
)
執行:
python3 create_database.py
應看到:
Database initialized.
接著:
ls -lh
會看到:
environment.db
二十六、IF NOT EXISTS 有什麼作用?
我們使用:
CREATE TABLE IF NOT EXISTS
表示:
如果 Table 不存在
→ 建立
如果已經存在
→ 不重複建立
因此每次程式啟動時都可以安全執行:
initialize_database()
不用擔心 Table 已存在而產生錯誤。
二十七、第一筆測試資料
建立:
nano insert_test.py
輸入:
import sqlite3
from datetime import datetime
DB_FILE = "environment.db"
temperature = 28.5
humidity = 67.2
timestamp = datetime.now().strftime(
"%Y-%m-%d %H:%M:%S"
)
connection = sqlite3.connect(
DB_FILE
)
cursor = connection.cursor()
cursor.execute(
"""
INSERT INTO environment_data
(
timestamp,
temperature,
humidity
)
VALUES
(
?,
?,
?
)
""",
(
timestamp,
temperature,
humidity
)
)
connection.commit()
connection.close()
print(
"Sensor data inserted."
)
執行:
python3 insert_test.py
二十八、為什麼 VALUES 使用問號?
程式:
VALUES (?, ?, ?)
然後另外傳入:
(
timestamp,
temperature,
humidity
)
這叫做:
Parameterized Query
不要寫成:
sql = (
"INSERT INTO environment_data "
"VALUES ('"
+ user_data
+ "')"
)
Parameter Binding 可以:
- 正確處理資料型別
- 正確處理特殊字元
- 降低 SQL Injection 風險
- 讓程式更加清楚
因此 Python SQLite 操作應養成:
cursor.execute(
SQL,
parameters
)
的習慣。
二十九、為什麼需要 commit()?
SQL:
INSERT
UPDATE
DELETE
會修改 Database。
執行:
connection.commit()
表示:
正式提交 Transaction。
如果忘記:
connection.commit()
資料可能沒有真正寫進 Database。
三十、讀取 SQLite 資料
建立:
nano query_data.py
輸入:
import sqlite3
DB_FILE = "environment.db"
connection = sqlite3.connect(
DB_FILE
)
cursor = connection.cursor()
cursor.execute(
"""
SELECT
id,
timestamp,
temperature,
humidity
FROM environment_data
ORDER BY id DESC
"""
)
rows = cursor.fetchall()
for row in rows:
print(row)
connection.close()
執行:
python3 query_data.py
可能看到:
(1, '2026-08-18 08:45:00', 28.5, 67.2)
三十一、fetchall() 是什麼?
執行:
cursor.fetchall()
表示取得:
所有查詢結果。
如果 SQL 查出:
1000 筆
fetchall() 就會一次取得這些資料。
對小型 Sensor Database 沒問題。
但大型系統不應每次都:
SELECT *
然後取得幾十萬筆紀錄。
應使用:
WHERE
LIMIT
Pagination
Aggregation
控制資料量。
三十二、取得最新一筆資料
可以:
cursor.execute(
"""
SELECT
timestamp,
temperature,
humidity
FROM environment_data
ORDER BY id DESC
LIMIT 1
"""
)
接著:
row = cursor.fetchone()
這非常適合 Web Dashboard 顯示:
Current Temperature
Current Humidity
三十三、格式化顯示資料
例如:
if row:
timestamp = row[0]
temperature = row[1]
humidity = row[2]
print(
f"Time: {timestamp}"
)
print(
f"Temperature: "
f"{temperature:.1f} °C"
)
print(
f"Humidity: "
f"{humidity:.1f} %"
)
畫面:
Time: 2026-08-18 08:45:00
Temperature: 28.5 °C
Humidity: 67.2 %
三十四、查詢高溫紀錄
例如找:
>= 30°C
SQL:
SELECT
timestamp,
temperature,
humidity
FROM environment_data
WHERE temperature >= 30
ORDER BY timestamp DESC;
Python:
cursor.execute(
"""
SELECT
timestamp,
temperature,
humidity
FROM environment_data
WHERE temperature >= ?
ORDER BY timestamp DESC
""",
(30.0,)
)
這也示範:
SELECT 查詢一樣可以使用 Parameterized Query。
三十五、統計最高溫
SQL:
SELECT MAX(temperature)
FROM environment_data;
Python:
cursor.execute(
"""
SELECT MAX(temperature)
FROM environment_data
"""
)
row = cursor.fetchone()
print(
"Maximum temperature:",
row[0]
)
三十六、統計最低溫
SELECT MIN(temperature)
FROM environment_data;
三十七、計算平均溫度
SELECT AVG(temperature)
FROM environment_data;
例如:
Average temperature:
28.36°C
三十八、總共記錄多少筆?
SQL:
SELECT COUNT(*)
FROM environment_data;
這對 Dashboard 很實用。
例如:
Total Records
8640
三十九、完整環境統計 SQL
可以一次取得:
SELECT
COUNT(*),
MIN(temperature),
MAX(temperature),
AVG(temperature),
MIN(humidity),
MAX(humidity),
AVG(humidity)
FROM environment_data;
如此可以得到:
Records:
1440
Temperature Min:
25.8°C
Temperature Max:
31.2°C
Temperature Avg:
28.1°C
Humidity Min:
54%
Humidity Max:
78%
Humidity Avg:
65%
四十、正式進入 Sensor Data Logging
現在將 SQLite 與 DHT11 / DHT22 整合。
系統:
DHT22
│
│ GPIO4
▼
Python
│
▼
Temperature
Humidity
│
▼
SQLite INSERT
│
▼
environment.db
這就是:
Data Logger
四十一、DHT22 接線
延續前一章:
| DHT22 | Raspberry Pi |
|---|---|
| VCC | 3.3V |
| DATA | GPIO4 |
| GND | GND |
架構:
Raspberry Pi
│
├── 3.3V ─────► DHT VCC
│
├── GPIO4 ────► DHT DATA
│
└── GND ──────► DHT GND
如果使用:
DHT Module
通常模組已整合必要元件。
若使用裸 Sensor,請依模組規格確認 DATA Pull-up。
四十二、完整 SQLite Sensor Logger
建立:
nano sensor_logger.py
輸入:
import sqlite3
import time
from datetime import datetime
import board
import adafruit_dht
# ======================================
# Settings
# ======================================
DB_FILE = "environment.db"
LOG_INTERVAL = 60
# ======================================
# DHT22
# GPIO4
# ======================================
dht = adafruit_dht.DHT22(
board.D4,
use_pulseio=False
)
# ======================================
# Database Initialization
# ======================================
def initialize_database():
connection = sqlite3.connect(
DB_FILE
)
cursor = connection.cursor()
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS
environment_data
(
id INTEGER
PRIMARY KEY
AUTOINCREMENT,
timestamp TEXT
NOT NULL,
temperature REAL
NOT NULL,
humidity REAL
NOT NULL
)
"""
)
connection.commit()
connection.close()
# ======================================
# Save Sensor Data
# ======================================
def save_sensor_data(
temperature,
humidity
):
timestamp = datetime.now().strftime(
"%Y-%m-%d %H:%M:%S"
)
connection = sqlite3.connect(
DB_FILE
)
cursor = connection.cursor()
cursor.execute(
"""
INSERT INTO environment_data
(
timestamp,
temperature,
humidity
)
VALUES
(
?,
?,
?
)
""",
(
timestamp,
temperature,
humidity
)
)
connection.commit()
connection.close()
print(
"Saved:",
timestamp,
f"{temperature:.1f} C",
f"{humidity:.1f} %"
)
# ======================================
# Main
# ======================================
initialize_database()
print(
"Raspberry Pi "
"Environment Logger Started"
)
try:
while True:
try:
temperature = (
dht.temperature
)
humidity = (
dht.humidity
)
if (
temperature is not None
and
humidity is not None
):
save_sensor_data(
temperature,
humidity
)
except RuntimeError as error:
print(
"DHT temporary error:",
error
)
except Exception as error:
print(
"Sensor error:",
error
)
time.sleep(
LOG_INTERVAL
)
except KeyboardInterrupt:
print(
"\nStopping logger..."
)
finally:
dht.exit()
print(
"Environment Logger Stopped"
)
四十三、執行 Sensor Logger
如果你在前一章已建立 Virtual Environment:
source ~/mqtt_project/venv/bin/activate
進入:
cd ~/environment_database
執行:
python sensor_logger.py
正常可能看到:
Raspberry Pi Environment Logger Started
Saved:
2026-08-18 08:45:00
28.5 C
67.2 %
Saved:
2026-08-18 08:46:00
28.6 C
67.1 %
因為:
LOG_INTERVAL = 60
所以:
每 60 秒記錄一次
四十四、如果使用 DHT11
將:
dht = adafruit_dht.DHT22(
board.D4,
use_pulseio=False
)
改成:
dht = adafruit_dht.DHT11(
board.D4,
use_pulseio=False
)
其他 Database 程式不用改。
這就是把:
Sensor Layer
與:
Database Layer
分離的好處。
四十五、資料實際會變成什麼?
假設執行 5 分鐘:
08:45
08:46
08:47
08:48
08:49
Database:
| id | timestamp | temperature | humidity |
|---|---|---|---|
| 1 | 2026-08-18 08:45:00 | 28.5 | 67.2 |
| 2 | 2026-08-18 08:46:00 | 28.6 | 67.1 |
| 3 | 2026-08-18 08:47:00 | 28.6 | 67.0 |
| 4 | 2026-08-18 08:48:00 | 28.7 | 66.8 |
| 5 | 2026-08-18 08:49:00 | 28.8 | 66.7 |
這就形成:
Time Series Data。
四十六、什麼是 Time Series Data?
Time Series Data 中文通常稱為:
時間序列資料。
例如:
時間 溫度
08:00 27.8
08:01 27.9
08:02 28.0
08:03 28.1
這種資料最重要的特徵就是:
每一筆資料都和時間有關。
IoT Sensor Data 幾乎都是典型 Time Series Data。
四十七、建立歷史資料查詢程式
建立:
nano history.py
輸入:
import sqlite3
DB_FILE = "environment.db"
connection = sqlite3.connect(
DB_FILE
)
cursor = connection.cursor()
cursor.execute(
"""
SELECT
id,
timestamp,
temperature,
humidity
FROM environment_data
ORDER BY id DESC
LIMIT 20
"""
)
rows = cursor.fetchall()
print(
"Latest 20 records"
)
print(
"-" * 65
)
for row in rows:
record_id = row[0]
timestamp = row[1]
temperature = row[2]
humidity = row[3]
print(
f"{record_id:5d} "
f"{timestamp} "
f"{temperature:6.1f} C "
f"{humidity:6.1f} %"
)
connection.close()
執行:
python3 history.py
可能看到:
Latest 20 records
-----------------------------------------------------------------
125 2026-08-18 08:49:00 28.8 C 66.7 %
124 2026-08-18 08:48:00 28.7 C 66.8 %
123 2026-08-18 08:47:00 28.6 C 67.0 %
四十八、查今天的資料
因為 Timestamp 使用:
YYYY-MM-DD HH:MM:SS
可以用:
WHERE date(timestamp) = date('now', 'localtime')
例如:
cursor.execute(
"""
SELECT
timestamp,
temperature,
humidity
FROM environment_data
WHERE
date(timestamp)
=
date(
'now',
'localtime'
)
ORDER BY timestamp ASC
"""
)
即可查詢:
今天的環境資料。
四十九、查最近一小時資料
可以使用:
SELECT *
FROM environment_data
WHERE datetime(timestamp)
>= datetime('now', '-1 hour', 'localtime');
這類查詢非常適合未來的:
Flask Dashboard
例如:
最近 1 小時
最近 6 小時
最近 24 小時
五十、建立環境統計程式
建立:
nano statistics.py
輸入:
import sqlite3
DB_FILE = "environment.db"
connection = sqlite3.connect(
DB_FILE
)
cursor = connection.cursor()
cursor.execute(
"""
SELECT
COUNT(*),
MIN(temperature),
MAX(temperature),
AVG(temperature),
MIN(humidity),
MAX(humidity),
AVG(humidity)
FROM environment_data
"""
)
row = cursor.fetchone()
if row:
record_count = row[0]
temp_min = row[1]
temp_max = row[2]
temp_avg = row[3]
humidity_min = row[4]
humidity_max = row[5]
humidity_avg = row[6]
print(
"Total Records:",
record_count
)
if temp_avg is not None:
print(
f"Temperature Min: "
f"{temp_min:.1f} C"
)
print(
f"Temperature Max: "
f"{temp_max:.1f} C"
)
print(
f"Temperature Avg: "
f"{temp_avg:.1f} C"
)
print(
f"Humidity Min: "
f"{humidity_min:.1f} %"
)
print(
f"Humidity Max: "
f"{humidity_max:.1f} %"
)
print(
f"Humidity Avg: "
f"{humidity_avg:.1f} %"
)
connection.close()
五十一、查詢高溫警報資料
例如:
30°C
以上屬於高溫。
SQL:
SELECT
timestamp,
temperature,
humidity
FROM environment_data
WHERE temperature >= 30
ORDER BY timestamp DESC;
這可以回答:
最近一次超過 30°C 是什麼時候?
也能進一步配合第 20 章:
LED Warning
Buzzer
Relay Fan
五十二、將設備狀態一起存進 Database
第 20 章不只要存 Sensor。
還可以增加:
led_status
fan_status
alarm_status
資料表:
CREATE TABLE environment_data
(
id INTEGER
PRIMARY KEY
AUTOINCREMENT,
timestamp TEXT
NOT NULL,
temperature REAL,
humidity REAL,
led_status INTEGER,
fan_status INTEGER,
alarm_status INTEGER
);
例如:
| time | temp | humidity | fan |
|---|---|---|---|
| 09:00 | 28.5 | 67 | 0 |
| 09:05 | 30.2 | 66 | 1 |
| 09:10 | 30.8 | 65 | 1 |
| 09:15 | 29.4 | 64 | 0 |
如此未來可以分析:
哪一次高溫造成 Fan ON?
五十三、Camera 事件也可以存 Database
第 16 章有:
Picamera2
因此未來可以建立 Camera Event Table。
例如:
CREATE TABLE camera_events
(
id INTEGER
PRIMARY KEY
AUTOINCREMENT,
timestamp TEXT
NOT NULL,
filename TEXT
NOT NULL,
event_type TEXT
);
資料:
| id | timestamp | filename | event_type |
|---|---|---|---|
| 1 | 2026-08-18 09:00 | photo001.jpg | manual |
| 2 | 2026-08-18 09:10 | photo002.jpg | high_temperature |
這樣就能知道:
這張照片為什麼被拍攝。
五十四、MQTT 與 SQLite 同時使用
上一章的:
MQTT
不需要被 SQLite 取代。
正確架構是:
DHT22
│
▼
Python
│
┌────────┴────────┐
│ │
▼ ▼
SQLite MQTT
│ │
▼ ▼
History Data Live Data
因此:
SQLite
=
Historical Storage
而:
MQTT
=
Real-Time Messaging
五十五、Flask + SQLite
第 17 章已學 Flask。
現在 Flask 就可以讀 SQLite。
架構:
SQLite
│
▼
Python
│
▼
Flask
│
▼
HTML
│
▼
Browser
例如 Web Page:
Raspberry Pi Environment Monitor
Current Temperature
28.6°C
Current Humidity
67.1%
Today's Maximum
31.2°C
Today's Minimum
25.9°C
Total Records
1438
這就是第 20 章 Dashboard 的資料來源。
五十六、不要讓 Web Page 直接操作 Sensor
比較好的架構不是:
Browser
↓
Flask
↓
每次重新讀 DHT22
而是:
Sensor Service
│
▼
SQLite
│
▼
Flask
│
▼
Browser
因為 Sensor Logger 可以:
持續記錄
而 Flask 只是:
讀取最新資料
兩個功能分開,架構更加清楚。
五十七、CSV 是什麼?
CSV 是:
Comma-Separated Values
例如:
id,timestamp,temperature,humidity
1,2026-08-18 08:45:00,28.5,67.2
2,2026-08-18 08:46:00,28.6,67.1
3,2026-08-18 08:47:00,28.6,67.0
它的優點是可以被很多軟體讀取。
例如:
- Microsoft Excel
- LibreOffice Calc
- Python pandas
- Google Sheets
- Database Tool
因此 IoT Database 很常提供:
Export CSV
功能。
五十八、建立 CSV 匯出程式
建立:
nano export_csv.py
輸入:
import csv
import sqlite3
from datetime import datetime
DB_FILE = "environment.db"
timestamp = datetime.now().strftime(
"%Y%m%d_%H%M%S"
)
CSV_FILE = (
f"environment_"
f"{timestamp}.csv"
)
connection = sqlite3.connect(
DB_FILE
)
cursor = connection.cursor()
cursor.execute(
"""
SELECT
id,
timestamp,
temperature,
humidity
FROM environment_data
ORDER BY id ASC
"""
)
rows = cursor.fetchall()
with open(
CSV_FILE,
"w",
newline="",
encoding="utf-8-sig"
) as csv_file:
writer = csv.writer(
csv_file
)
writer.writerow(
[
"ID",
"Timestamp",
"Temperature_C",
"Humidity_Percent"
]
)
writer.writerows(
rows
)
connection.close()
print(
"CSV exported:",
CSV_FILE
)
五十九、為什麼使用 utf-8-sig?
程式:
encoding="utf-8-sig"
主要是為了讓某些 Windows / Excel 環境在開啟包含中文欄位的 CSV 時,比單純 UTF-8 更容易正確辨識文字編碼。
如果全部欄位都是英文,也可以:
encoding="utf-8"
六十、執行 CSV Export
執行:
python3 export_csv.py
可能得到:
CSV exported:
environment_20260818_084500.csv
查看:
ls -lh *.csv
六十一、CSV 可以做什麼?
把 CSV 複製到電腦後,可以使用 Excel 建立:
Temperature Chart
Humidity Chart
Daily Average
Daily Maximum
Trend Analysis
例如:
Temperature
31 ┤ ╭──╮
30 ┤ ╭───╯ ╰─
29 ┤ ╭────╯
28 ┤───────╯
27 ┤
└──────────────────────
Time
這就從:
Sensor Reading
進一步變成:
Data Analysis。
六十二、SQLite Database 可以直接用 CLI 查看嗎?
如果系統安裝 SQLite Command Line Tool,可以:
sudo apt install -y sqlite3
然後:
sqlite3 environment.db
進入 SQLite Console。
輸入:
.tables
查看 Table。
六十三、查看 Table Schema
輸入:
.schema environment_data
可以看到資料表結構。
六十四、直接執行 SELECT
在 SQLite Console:
SELECT *
FROM environment_data
ORDER BY id DESC
LIMIT 10;
即可查看最新 10 筆紀錄。
離開:
.quit
六十五、讓 SQLite 顯示更容易閱讀
SQLite CLI 中可以:
.headers on
以及:
.mode column
接著:
SELECT
id,
timestamp,
temperature,
humidity
FROM environment_data
ORDER BY id DESC
LIMIT 10;
顯示會更加整齊。
六十六、Database 檔案放在哪裡?
目前:
DB_FILE = "environment.db"
代表 Database 建立在:
目前 Working Directory。
如果希望永遠放在固定位置,可以:
from pathlib import Path
DB_FILE = (
Path.home()
/ "environment_database"
/ "environment.db"
)
這樣不論從哪裡執行 Python,都會使用:
/home/使用者/environment_database/environment.db
正式專案通常建議使用:
Absolute Path
避免程式因 Working Directory 不同而產生多個 Database。
六十七、Database 檔案權限
SQLite 不是只有:
environment.db
檔案可讀寫就一定沒問題。
實際寫入時,程式也可能需要對:
Database 所在目錄
有寫入權限。
因為 SQLite 某些模式可能需要建立:
Journal
WAL
SHM
等輔助檔案。
所以正式使用 systemd Service 時,需要確認:
Service User
Database Directory Owner
Directory Permission
一致。
這是 Raspberry Pi 專案常見的部署問題。
六十八、不要使用 sudo 解決所有權限問題
如果 Python 出現:
Permission denied
不建議第一個反應就是:
sudo python3 sensor_logger.py
更好的方式是檢查:
ls -ld ~/environment_database
以及:
ls -l ~/environment_database
確認:
- Database Owner
- Directory Owner
- Service User
- File Permission
正確設定權限,比長期讓 Sensor Service 使用 root 執行更合理。
六十九、Database 會不會越來越大?
會。
假設:
每分鐘 1 筆
一天:
60 × 24
=
1440 筆
一年:
1440 × 365
=
525600 筆
SQLite 處理這類規模通常不成問題,但還是應從系統設計考慮:
- Logging Interval
- Retention Policy
- Database Backup
- Archive
- Cleanup
- Index
七十、記錄頻率應該設多少?
不是越快越好。
例如室內溫度:
每 10 毫秒
沒有太大意義。
常見參考:
| 用途 | 建議 Interval |
|---|---|
| Demo | 2~10 秒 |
| 一般室內監測 | 30~60 秒 |
| 長期環境監測 | 1~5 分鐘 |
| 非快速變化環境 | 5~15 分鐘 |
如果:
LOG_INTERVAL = 60
代表每分鐘一筆。
通常已經非常適合一般環境監測。
七十一、避免儲存完全相同的資料
更進階的設計可以判斷:
Temperature Change
Humidity Change
例如只有:
Temperature 改變 >= 0.2°C
才新增資料。
或者:
每 5 分鐘至少記一筆
+
有重大變化立即記錄
如此可以兼顧:
歷史完整性
+
Database Size
七十二、什麼是 Index?
如果 Database 變大,常常需要快速依時間查詢。
可以建立:
CREATE INDEX IF NOT EXISTS
idx_environment_timestamp
ON environment_data(timestamp);
如此大量資料環境中:
WHERE timestamp ...
或:
ORDER BY timestamp
可能獲得更好的查詢效率。
對本章幾百、幾千筆實驗資料不是必須,但這是未來系統擴充的重要 Database 概念。
七十三、完整初始化函式可以加入 Index
例如:
cursor.execute(
"""
CREATE INDEX IF NOT EXISTS
idx_environment_timestamp
ON environment_data(timestamp)
"""
)
因此 Database 初始化包含:
CREATE TABLE
+
CREATE INDEX
七十四、SQLite Transaction
如果每次只寫一筆:
connect
INSERT
commit
close
非常容易理解。
如果一次匯入大量資料,可以使用:
Transaction
將多筆 INSERT 一次提交,提高效率。
例如:
connection = sqlite3.connect(
DB_FILE
)
cursor = connection.cursor()
for item in sensor_data:
cursor.execute(
SQL,
item
)
connection.commit()
connection.close()
不要每筆都重新開啟 Database。
七十五、SQLite Context Manager
Python 還可以使用:
with sqlite3.connect(
DB_FILE
) as connection:
例如:
def save_sensor_data(
temperature,
humidity
):
timestamp = datetime.now().strftime(
"%Y-%m-%d %H:%M:%S"
)
with sqlite3.connect(
DB_FILE
) as connection:
connection.execute(
"""
INSERT INTO environment_data
(
timestamp,
temperature,
humidity
)
VALUES
(
?,
?,
?
)
""",
(
timestamp,
temperature,
humidity
)
)
這種寫法更適合後續整理大型專案。
七十六、NULL Sensor Data 不應亂寫入
如果 DHT Reading 失敗:
temperature = None
不要直接把:
NULL
當成有效測量紀錄寫進 Database。
本章已使用:
if (
temperature is not None
and
humidity is not None
):
確認兩個值都有效才:
INSERT
這是 Data Quality 很重要的一環。
七十七、Sensor Error 可以另建 Event Table
進階系統可以建立:
CREATE TABLE system_events
(
id INTEGER
PRIMARY KEY
AUTOINCREMENT,
timestamp TEXT
NOT NULL,
event_type TEXT,
message TEXT
);
例如:
2026-08-18 09:20:00
SENSOR_ERROR
DHT timeout
或:
2026-08-18 09:30:00
FAN_ON
Temperature reached 30.2 C
因此 Database 不只是:
Sensor Database
也可以成為:
System Event Log。
七十八、Database 與 Log File 的差別
文字 Log:
2026-08-18 08:45 Temp=28.5 Humidity=67.2
2026-08-18 08:46 Temp=28.6 Humidity=67.1
很適合:
Debug
System Log
Database:
timestamp
temperature
humidity
則更加適合:
搜尋
排序
篩選
統計
Web Dashboard
報表
CSV Export
完整系統可以同時使用兩者。
七十九、備份 SQLite Database
因為 SQLite Database 是檔案,所以最簡單的備份可以是:
cp environment.db \
environment_backup.db
也可以加入時間:
cp environment.db \
environment_$(date +%Y%m%d_%H%M%S).db
例如:
environment_20260818_084500.db
八十、不要只依賴 microSD
如果 Raspberry Pi 長期運作,而且資料非常重要,不應只依賴單一 microSD Card。
可以考慮:
Raspberry Pi
│
├── Local SQLite
│
└── Backup
│
▼
NAS / Server
或:
SQLite
↓
定期 CSV Export
↓
Remote Storage
第 20 章可以先以本機 SQLite 為主。
正式長期系統則需要額外 Backup Strategy。
八十一、如何搭配 MQTT?
可以把 Sensor Worker 設計成:
Read DHT22
│
├── SQLite INSERT
│
└── MQTT Publish
概念:
temperature = read_temperature()
humidity = read_humidity()
save_sensor_data(
temperature,
humidity
)
mqtt_client.publish(
TEMP_TOPIC,
temperature
)
這樣一次測量就同時提供:
Live Data
+
History Data
八十二、如何搭配 Flask?
Flask 首頁可以執行:
SELECT
timestamp,
temperature,
humidity
FROM environment_data
ORDER BY id DESC
LIMIT 1;
取得最新資料。
歷史頁可以:
SELECT *
FROM environment_data
ORDER BY timestamp DESC
LIMIT 100;
統計頁:
SELECT
MIN(temperature),
MAX(temperature),
AVG(temperature)
FROM environment_data;
因此 Flask 很自然就變成:
SQLite Database Frontend。
八十三、第 20 章最終架構已經形成
目前第 16~19 章:
第 16 章
Camera
Picamera2
│
第 17 章
Flask
Web Server
│
第 18 章
MQTT
Mosquitto
│
第 19 章
SQLite
Database
現在可以合併:
Raspberry Pi
│
┌──────────────┼──────────────┐
│ │ │
Sensor Camera GPIO
│ │ │
▼ ▼ ▼
DHT22 Picamera2 LED/Relay
│ │ │
└──────────────┼──────────────┘
│
Python
│
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
Flask MQTT SQLite
│ │ │
▼ ▼ ▼
Web IoT Message History Data
這就是第 20 章:
Raspberry Pi 智慧環境監控中心
的完整軟體骨架。
八十四、本章完整資料流程
Sensor Reading:
DHT22
│
▼
GPIO4
│
▼
Python
│
▼
temperature
humidity
加入時間:
datetime.now()
│
▼
2026-08-18 08:45:00
寫入:
Timestamp
Temperature
Humidity
│
▼
SQL INSERT
│
▼
SQLite
│
▼
environment.db
查詢:
environment.db
│
▼
SQL SELECT
│
▼
Python
│
▼
Terminal / Flask / CSV
這就是完整:
Sensor Data Logging Pipeline
八十五、本章常用 SQL 整理
建立 Table:
CREATE TABLE IF NOT EXISTS environment_data
(
id INTEGER
PRIMARY KEY
AUTOINCREMENT,
timestamp TEXT
NOT NULL,
temperature REAL,
humidity REAL
);
新增:
INSERT INTO environment_data
(
timestamp,
temperature,
humidity
)
VALUES
(
?,
?,
?
);
全部查詢:
SELECT *
FROM environment_data;
最新 10 筆:
SELECT *
FROM environment_data
ORDER BY id DESC
LIMIT 10;
高溫:
SELECT *
FROM environment_data
WHERE temperature >= 30;
平均:
SELECT AVG(temperature)
FROM environment_data;
最高:
SELECT MAX(temperature)
FROM environment_data;
最低:
SELECT MIN(temperature)
FROM environment_data;
總筆數:
SELECT COUNT(*)
FROM environment_data;
八十六、本章常用指令整理
建立專案:
mkdir -p ~/environment_database
進入:
cd ~/environment_database
建立資料庫:
python3 create_database.py
寫入測試資料:
python3 insert_test.py
查詢:
python3 query_data.py
啟動 Sensor Logger:
python sensor_logger.py
查看歷史:
python3 history.py
查看統計:
python3 statistics.py
匯出 CSV:
python3 export_csv.py
安裝 SQLite CLI:
sudo apt update
sudo apt install -y sqlite3
開啟 Database:
sqlite3 environment.db
八十七、本章常見錯誤:no such table
如果出現:
sqlite3.OperationalError:
no such table: environment_data
代表目前開啟的 Database 中沒有:
environment_data
檢查:
Database 路徑是否正確
Table 是否初始化
是否開到了另一個 environment.db
這也是為什麼正式程式應在啟動時執行:
initialize_database()
八十八、常見錯誤:database is locked
如果出現:
database is locked
可能原因:
- 另一個程式長時間持有 Transaction
- Connection 沒有正常關閉
- 多個 Process 同時大量寫入
- 程式發生 Exception 後未正確 Cleanup
簡單 Sensor Logger 通常不容易遇到。
但應養成:
connection.commit()
connection.close()
或使用:
with sqlite3.connect(...)
的習慣。
八十九、常見錯誤:unable to open database file
如果:
sqlite3.OperationalError:
unable to open database file
常見原因:
目錄不存在
目錄無寫入權限
Database Path 錯誤
Service User 權限不足
先:
ls -ld ~/environment_database
再:
ls -l ~/environment_database
確認檔案與目錄權限。
九十、常見錯誤:資料一直沒有增加
先確認 Sensor:
是否正常讀到 Temperature / Humidity
再確認:
save_sensor_data()
是否真的執行。
最後確認:
connection.commit()
是否存在。
可以先加入:
print(
temperature,
humidity
)
確認資料流。
九十一、Database Debug 建議順序
出問題時不要一次檢查全部系統。
建議:
1. Sensor Test
↓
2. SQLite CREATE
↓
3. 手動 INSERT
↓
4. SELECT
↓
5. Sensor Logger
↓
6. Flask
↓
7. MQTT
也就是:
分層測試。
這會比直接 Debug 完整 IoT 系統容易很多。
九十二、實作成果
本章完成:
Raspberry Pi 溫溼度歷史資料庫
具備:
- Raspberry Pi 讀取 DHT11 / DHT22
- GPIO4 接收 Sensor Data
- Python 建立 Timestamp
- SQLite 建立 Database
- 自動建立 Table
- 定期 INSERT 溫度
- 定期 INSERT 濕度
- SELECT 查詢歷史資料
- 查詢最新資料
- 查詢高溫紀錄
- MAX / MIN / AVG 統計
- COUNT 計算資料筆數
- CSV 匯出
- Database Backup 基本概念
- 為 Flask Dashboard 提供資料
- 為第 20 章完整 IoT 專題提供 History Layer
九十三、本章重點整理
本章最核心的觀念是:
Sensor Data
不只需要即時傳送
還需要長期保存
MQTT 解決:
資料如何即時傳送
SQLite 解決:
資料如何長期保存與查詢
完整架構:
Sensor
│
▼
Python
│
├────► MQTT
│ │
│ ▼
│ Real-Time
│
└────► SQLite
│
▼
Historical
這是一個非常典型的:
IoT Data Architecture。
九十四、下一章預告
下一章將正式進入本系列最重要的:
第 20 章 Raspberry Pi 綜合專題:智慧環境監控與遠端控制系統
我們會把前面章節全部整合。
包含:
Raspberry Pi
Raspberry Pi OS
Linux
Python
GPIO
DHT11 / DHT22
LCD1602 I²C
LED
Buzzer
Relay
Camera
Flask
MQTT
SQLite
SSH
systemd
建立:
Raspberry Pi 智慧環境監控中心
功能包含:
- 讀取室內溫度、濕度
- LCD 即時顯示
- 溫度過高 LED 警示
- 蜂鳴器警報
- 自動啟動繼電器/風扇
- SQLite 記錄歷史資料
- Web 顯示目前狀態
- 手機遠端控制設備
- MQTT 發布感測資料
- Camera 遠端拍照
- SSH 遠端維護
- 開機自動啟動 Service
整體將形成:
Raspberry Pi
│
┌──────────────┼──────────────┐
│ │ │
DHT22 Camera GPIO
│ │ │
▼ ▼ ▼
Temp/Humidity Picamera2 LED/Buzzer/Relay
│ │ │
└──────────────┼──────────────┘
│
Python
│
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
Flask MQTT SQLite
│ │ │
▼ ▼ ▼
Smartphone IoT Client History Data
這將真正完成一套:
Raspberry Pi IoT 智慧環境監控系統。