Quản lý nhiều file database với ATTACH và DETACH
Sử dụng lệnh ATTACH để gắn kết nhiều file DuckDB (.duckdb) vào cùng một phiên làm việc, giúp di chuyển dữ liệu giữa các file hoặc hợp nhất báo cáo.
Thực hiện lệnh này trong CLI của DuckDB hoặc qua Python connector. Đây là nền tảng để thực hiện sao lưu dữ liệu vào một file mới hoặc tách dữ liệu ra file riêng.
duckdb /var/lib/duckdb/production.duckdb
ATTACH '/var/lib/duckdb/backup/production_backup_v1.duckdb' AS backup_db;
Kết quả mong đợi: Không có lỗi, prompt CLI vẫn giữ nguyên, bạn đã có thể truy cập schema của database mới gắn vào với tên alias là backup_db.
Sao chép toàn bộ dữ liệu từ database chính sang database sao lưu bằng lệnh INSERT INTO ... SELECT qua các schema đã gắn.
INSERT INTO backup_db.my_table SELECT * FROM production.my_table;
Kết quả mong đợi: Thông báo số lượng dòng được chèn thành công (ví dụ: "10000 rows inserted").
Sử dụng lệnh DETACH để ngắt kết nối với file database đã gắn, giải phóng tài nguyên và đảm bảo dữ liệu đã được đóng file an toàn.
DETACH backup_db;
.quit
Kết quả mong đợi: CLI thoát ra, file production_backup_v1.duckdb được tạo trong thư mục /var/lib/duckdb/backup/ và sẵn sàng để di chuyển.
Để xác minh kết quả, mở trực tiếp file backup và kiểm tra số lượng dòng.
duckdb /var/lib/duckdb/backup/production_backup_v1.duckdb
SELECT COUNT(*) FROM my_table;
Kết quả mong đợi: Số lượng dòng trả về bằng với số lượng dòng của database gốc trước khi backup.
Script Python tự động hóa Backup và Restore
Viết script Python sử dụng thư viện duckdb để tự động hóa quy trình sao lưu (backup) và khôi phục (restore) dữ liệu.
Script này sẽ thực hiện: Mở DB gốc -> ATTACH DB backup -> Copy dữ liệu -> DETACH. Đối với Restore, nó sẽ làm ngược lại: ATTACH DB backup -> Copy dữ liệu vào DB gốc.
Tạo file script tại /opt/scripts/duckdb_backup.py với nội dung hoàn chỉnh sau.
#!/usr/bin/env python3
import duckdb
import sys
import os
from datetime import datetime
# Cấu hình đường dẫn
PROD_DB_PATH = "/var/lib/duckdb/production.duckdb"
BACKUP_DIR = "/var/lib/duckdb/backup"
TABLE_NAME = "my_table" # Thay đổi theo tên bảng thực tế của bạn
def ensure_dir(path):
if not os.path.exists(path):
os.makedirs(path)
def backup_database(source_path, target_path):
print(f"[*] Bắt đầu backup từ {source_path} sang {target_path}")
try:
# Mở DB nguồn
con = duckdb.connect(source_path)
# Kiểm tra file đích có tồn tại không, nếu có thì xóa để tạo mới sạch sẽ (hoặc dùng merge logic phức tạp hơn)
if os.path.exists(target_path):
os.remove(target_path)
# Gắn DB đích vào session
con.execute(f"ATTACH '{target_path}' AS backup_db")
# Sao chép dữ liệu (Copy toàn bộ bảng)
# Lưu ý: Cần tạo schema tương tự trong backup_db nếu chưa có
# Ở đây ta dùng INSERT INTO ... SELECT FROM ...
# DuckDB tự động tạo bảng đích nếu dùng CREATE TABLE AS SELECT, nhưng để giữ cấu trúc:
con.execute(f"CREATE TABLE backup_db.{TABLE_NAME} AS SELECT * FROM main.{TABLE_NAME}")
con.execute("DETACH backup_db")
con.close()
print(f"[+] Backup thành công. File: {target_path}")
return True
except Exception as e:
print(f"[-] Lỗi khi backup: {str(e)}")
return False
def restore_database(source_path, target_path, overwrite=True):
print(f"[*] Bắt đầu restore từ {source_path} vào {target_path}")
try:
con = duckdb.connect(target_path)
con.execute(f"ATTACH '{source_path}' AS backup_db")
if overwrite:
# Xóa dữ liệu cũ trong bảng đích
con.execute(f"DELETE FROM main.{TABLE_NAME}")
# Chèn dữ liệu từ backup vào
con.execute(f"INSERT INTO main.{TABLE_NAME} SELECT * FROM backup_db.{TABLE_NAME}")
con.execute("DETACH backup_db")
con.close()
print(f"[+] Restore thành công. Dữ liệu đã đưa về {target_path}")
return True
except Exception as e:
print(f"[-] Lỗi khi restore: {str(e)}")
return False
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Sử dụng: python duckdb_backup.py [backup|restore] [filename]")
sys.exit(1)
action = sys.argv[1]
filename = sys.argv[2] if len(sys.argv) > 2 else f"{TABLE_NAME}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.duckdb"
ensure_dir(BACKUP_DIR)
target_path = os.path.join(BACKUP_DIR, filename)
if action == "backup":
backup_database(PROD_DB_PATH, target_path)
elif action == "restore":
restore_database(target_path, PROD_DB_PATH)
else:
print("Lệnh không hợp lệ. Chọn 'backup' hoặc 'restore'")
sys.exit(1)
Kết quả mong đợi: Script chạy không lỗi, in ra log trạng thái "[+] Backup thành công" hoặc "[+] Restore thành công".
Thiết lập quyền thực thi cho script.
chmod +x /opt/scripts/duckdb_backup.py
Kết quả mong đợi: File script có thể chạy trực tiếp như một command.
Test script bằng cách chạy backup thủ công.
/opt/scripts/duckdb_backup.py backup test_backup.duckdb
Kết quả mong đợi: File test_backup.duckdb được tạo trong thư mục /var/lib/duckdb/backup/.
Cấu hình Cron Job để sao lưu định kỳ
Sử dụng systemd timer thay vì cron truyền thống để quản lý task trên Ubuntu 24.04, vì nó tích hợp tốt hơn với service và có log chi tiết hơn.
Tạo file service để chạy script backup.
Tạo file /etc/systemd/system/duckdb-backup.service với nội dung sau.
[Unit]
Description=DuckDB Automated Backup Service
After=network.target
[Service]
Type=oneshot
User=root
WorkingDirectory=/var/lib/duckdb
ExecStart=/opt/scripts/duckdb_backup.py backup daily_backup_$(date +%F).duckdb
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
Kết quả mong đợi: File service được tạo, systemd nhận diện được unit mới.
Tạo file timer để kích hoạt service định kỳ (ví dụ: mỗi ngày lúc 2h sáng).
Tạo file /etc/systemd/system/duckdb-backup.timer với nội dung sau.
[Unit]
Description=Run DuckDB Backup Daily at 2 AM
Requires=duckdb-backup.service
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
RandomizedDelaySec=300
Unit=duckdb-backup.service
[Install]
WantedBy=timers.target
Kết quả mong đợi: File timer được tạo, cấu hình lịch chạy mỗi ngày lúc 02:00.
Enable và start timer để kích hoạt lập tức.
systemctl daemon-reload
systemctl enable duckdb-backup.timer
systemctl start duckdb-backup.timer
Kết quả mong đợi: Timer được bật, trạng thái là "active".
Test chạy thủ công bằng cách trigger service ngay lập tức để kiểm tra lỗi.
systemctl start duckdb-backup.service
Kết quả mong đợi: Command chạy xong, file backup mới được tạo trong thư mục đích.
Verify trạng thái timer và xem lịch sử chạy.
systemctl status duckdb-backup.timer
journalctl -u duckdb-backup.service -n 20
Kết quả mong đợi: Status hiển thị "next trigger" vào ngày mai 2h sáng, và log journal hiển thị kết quả chạy của script.
Chiến lược phân vùng dữ liệu (Partitioning) cho dữ liệu lớn
Đối với dữ liệu lớn (Big Data), DuckDB hoạt động hiệu quả nhất khi dữ liệu được tổ chức theo Partitioning (phân vùng) dựa trên một cột thời gian hoặc danh mục.
Thay vì một file database khổng lồ, chúng ta chia nhỏ thành nhiều file con (partitions) và sử dụng ATTACH để truy vấn tập hợp dữ liệu.
Tạo cấu trúc thư mục để lưu trữ các partition theo năm/tháng.
mkdir -p /var/lib/duckdb/partitions/2024/{01,02,03}
Kết quả mong đợi: Các thư mục con được tạo sẵn sàng để chứa file .duckdb.
Viết script Python để tạo dữ liệu mẫu và phân vùng chúng.
Tạo file /opt/scripts/duckdb_partitioning.py.
#!/usr/bin/env python3
import duckdb
import os
from datetime import datetime, timedelta
BASE_PATH = "/var/lib/duckdb/partitions"
TABLE_NAME = "sales_data"
TOTAL_ROWS = 1000000 # Giả lập 1 triệu dòng dữ liệu
def generate_partition_data(start_date, end_date, file_path, row_count):
"""Tạo file partition chứa dữ liệu trong khoảng thời gian cụ thể"""
con = duckdb.connect(file_path)
# Tạo bảng
con.execute(f"CREATE TABLE {TABLE_NAME} (id INT, date DATE, amount DECIMAL(10,2), region VARCHAR)")
# Sinh dữ liệu giả
dates = [start_date + timedelta(days=i) for i in range((end_date - start_date).days + 1)]
# Chèn dữ liệu (giả lập phân phối đều)
ids = list(range(row_count))
amounts = [float(i % 10000) for i in ids]
regions = ["North", "South", "East", "West"] * (row_count // 4 + 1)
data = list(zip(ids, dates * (row_count // len(dates)), amounts, regions))
con.executemany(f"INSERT INTO {TABLE_NAME} VALUES (?, ?, ?, ?)", data)
con.close()
print(f"[+] Created partition: {file_path} with {row_count} rows")
def create_partitions():
# Cấu hình các partition theo tháng
partitions_config = [
{"year": 2024, "month": 1, "rows": 10000},
{"year": 2024, "month": 2, "rows": 10000},
{"year": 2024, "month": 3, "rows": 10000},
]
for conf in partitions_config:
year = conf["year"]
month = conf["month"]
rows = conf["rows"]
dir_path = f"{BASE_PATH}/{year}/{str(month).zfill(2)}"
os.makedirs(dir_path, exist_ok=True)
file_name = f"data_{year}_{month}.duckdb"
file_path = os.path.join(dir_path, file_name)
# Tính ngày bắt đầu và kết thúc của tháng
start_date = datetime(year, month, 1)
if month == 12:
end_date = datetime(year + 1, 1, 1) - timedelta(days=1)
else:
end_date = datetime(year, month + 1, 1) - timedelta(days=1)
generate_partition_data(start_date, end_date, file_path, rows)
if __name__ == "__main__":
create_partitions()
Kết quả mong đợi: Script chạy xong, tạo ra 3 file database riêng biệt trong các thư mục tương ứng.
Thực thi script để tạo dữ liệu phân vùng.
/opt/scripts/duckdb_partitioning.py
Kết quả mong đợi: Log in ra thông báo tạo partition thành công cho tháng 1, 2, 3.
Truy vấn dữ liệu từ tất cả các partition cùng lúc bằng cách ATTACH tất cả file vào một session.
Tạo file script /opt/scripts/query_partitions.py để minh họa việc query cross-partition.
#!/usr/bin/env python3
import duckdb
import glob
# Tìm tất cả file partition
partition_files = glob.glob("/var/lib/duckdb/partitions/2024/**/*.duckdb", recursive=True)
print(f"[*] Found {len(partition_files)} partition files")
con = duckdb.connect() # Mở session trống
# Attach từng file với alias tên file (loại bỏ đuôi .duckdb)
for path in partition_files:
# Tạo alias an toàn (thay dấu / và . bằng _)
alias = path.replace("/", "_").replace(".", "_").replace(".duckdb", "")
con.execute(f"ATTACH '{path}' AS {alias}")
print(f"[+] Attached {alias}")
# Query tổng hợp từ tất cả các partition
# Lưu ý: Tên bảng phải giống nhau trong tất cả file (sales_data)
result = con.execute("""
SELECT SUM(amount) as total_revenue,
COUNT(*) as total_transactions
FROM (
SELECT amount FROM _var_lib_duckdb_partitions_2024_01_data_2024_1 WHERE 1=1
UNION ALL
SELECT amount FROM _var_lib_duckdb_partitions_2024_02_data_2024_2 WHERE 1=1
UNION ALL
SELECT amount FROM _var_lib_duckdb_partitions_2024_03_data_2024_3 WHERE 1=1
)
""").fetchall()
print(f"\n[+] Kết quả tổng hợp: {result}")
# Detach tất cả
for path in partition_files:
alias = path.replace("/", "_").replace(".", "_").replace(".duckdb", "")
con.execute(f"DETACH {alias}")
con.close()
Kết quả mong đợi: Script chạy, in ra tổng doanh thu (SUM) và tổng giao dịch (COUNT) từ cả 3 file partition.
Verify bằng cách kiểm tra kích thước file và số lượng dòng của từng partition riêng lẻ.
ls -lh /var/lib/duckdb/partitions/2024/*/
duckdb /var/lib/duckdb/partitions/2024/01/data_2024_1.duckdb -c "SELECT COUNT(*) FROM sales_data;"
Kết quả mong đợi: Mỗi file có kích thước nhỏ (vài MB thay vì hàng GB), và count trả về đúng số dòng đã sinh ra (10000).
Điều hướng series:
Mục lục: Series: Triển khai Database Serverless với DuckDB và Ubuntu 24.04
« Phần 4: Kết nối DuckDB từ các ứng dụng Python và SQL Client
Phần 6: Bảo mật, giám sát và xử lý sự cố (Troubleshooting) »