建立数据库连接
要使用 DuckDB,必须首先创建一个与数据库的连接。具体的语法因客户端 API 而异,但通常涉及传递一个参数来配置持久性。import duckdb
# 创建一个内存数据库
con = duckdb.connect(database=':memory:')
# 创建一个持久化数据库
con = duckdb.connect(database='my_database.duckdb')
模式
DuckDB 可以在两种模式下运行:持久模式(Persistent Mode),其中数据保存到磁盘;以及内存模式(In-Memory Mode),其中整个数据集存储在主内存中。- 持久模式(Persistent Mode):
- 数据保存到磁盘:在这种模式下,数据会被保存到磁盘上的一个数据库文件中。这意味着即使程序关闭或系统重启,数据仍然会保留。
- 适用场景:适用于需要长期存储和持久化数据的应用程序,例如数据分析、报表生成等。
- 内存模式(In-Memory Mode):
- 数据存储在主内存:在这种模式下,数据完全存储在主内存中。这意味着数据在程序关闭或系统重启后会丢失。
- 适用场景:适用于对性能要求较高且不需要持久化数据的应用程序,例如临时数据分析、快速查询等。
持久模式(Persistent Mode)
要创建或打开一个持久化数据库,需要在创建连接时设置数据库文件的路径,例如 `my_database.duckdb`。这个路径可以指向一个现有的数据库文件,也可以指向一个尚不存在的文件,DuckDB 会根据需要选择打开或创建数据库。数据库文件可以使用任意扩展名,但常见的扩展名包括 `.db`、`.duckdb` 和 `.ddb`。- 如果路径指向一个现有的数据库文件,DuckDB 将打开该文件。
- 如果路径指向一个尚不存在的文件,DuckDB 将创建一个新的数据库文件。
import duckdb
# 创建一个持久化数据库
con = duckdb.connect(database='my_database.duckdb')
内存模式(In-Memory Mode)
DuckDB 可以在内存模式(In-Memory Mode)下运行。在大多数客户端中,可以**通过传递特殊值 **`**:memory:**`** 作为数据库文件或省略数据库文件参数来激活内存模式**。在内存模式下,所有数据都存储在主内存中,因此当进程结束时,所有数据都会丢失。import duckdb
# 方式1:创建一个内存数据库
con = duckdb.connect(database=':memory:')
# 方式2:创建一个内存数据库
con = duckdb.connect()
溢出到磁盘
无论是持久模式还是内存模式,DuckDB 都使用“溢出到磁盘”(spilling to disk)机制来处理超出内存容量的工作负载。这种机制确保了即使数据集大小超过了可用内存,DuckDB 仍然能够高效地进行数据处理。溢出到磁盘(Spilling to Disk):
- 定义:溢出到磁盘是指当内存中的数据量超过可用内存时,DuckDB 将部分数据写入磁盘,以释放内存空间。
- 目的:通过将部分数据存储在磁盘上,DuckDB 可以处理比可用内存更大的数据集,从而支持更大规模的数据分析任务。
- 性能影响:虽然磁盘 I/O 操作通常比内存操作慢,但通过优化和缓存机制,DuckDB 尽量减少对性能的影响。
持久模式下的溢出机制
+ **数据存储**:数据已经存储在磁盘上的数据库文件中。 + **溢出机制**:当内存中的数据量超过可用内存时,DuckDB 可以继续使用磁盘上的空间来存储临时数据,而不需要额外的磁盘 I/O 操作来写入数据。内存模式下的溢出机制
+ **数据存储**:数据完全存储在主内存中。 + **溢出机制**:当内存中的数据量超过可用内存时,DuckDB 会将部分数据写入磁盘上的临时文件,以释放内存空间。这些临时文件通常存储在系统临时目录中。并发处理
DuckDB 提供了两种可配置的并发选项:- 单进程读写:
- 一个进程可以同时读取和写入数据库。
- 多进程只读:
- 多个进程可以读取数据库,但没有进程可以写入(
access_mode = 'READ_ONLY'
)。
- 多个进程可以读取数据库,但没有进程可以写入(
单进程并发
当使用单进程读写选项时,DuckDB 支持多线程写入,结合使用多版本并发控制MVCC[ (Multi-Version Concurrency Control)](https://en.wikipedia.org/wiki/Multiversion_concurrency_control)和乐观并发控制(Optimistic Concurrency Control)来管理并发操作。具体规则如下:- 多线程写入:
- 如果没有写冲突,多个并发写操作将成功。
- 追加操作(
INSERT
)不会冲突,即使在同一个表上。 - 多个线程可以同时更新不同的表或同一个表的不同子集。
- 乐观并发控制:
- 当两个线程尝试同时编辑(更新或删除)同一行时,乐观并发控制会介入。
- 第二个尝试编辑的线程将失败并抛出冲突错误。
多进程写入
DuckDB 不支持从多个进程自动写入同一个数据库文件,并且这也不是其主要设计目标。如果多个进程必须写入同一个文件,可以考虑以下设计模式:1、跨进程互斥锁:
- 每个进程获取一个跨进程互斥锁,然后以读写模式打开数据库,查询完成后关闭数据库。
import duckdb
import fcntl
# 获取文件锁
with open('lock_file', 'w') as lock_file:
fcntl.flock(lock_file, fcntl.LOCK_EX)
con = duckdb.connect(database='my_database.duckdb')
con.execute("INSERT INTO users VALUES (1, 'Alice')")
con.close()
fcntl.flock(lock_file, fcntl.LOCK_UN)
2、重试连接:
- 如果另一个进程已经连接到数据库,当前进程可以重试连接。
import duckdb
import time
while True:
try:
con = duckdb.connect(database='my_database.duckdb')
con.execute("INSERT INTO users VALUES (1, 'Alice')")
con.close()
break
except duckdb.IOException:
time.sleep(1) # 等待一段时间后重试
3、使用外部数据库:
- 使用 MySQL、PostgreSQL 或 SQLite 进行多进程事务管理,然后使用 DuckDB 的扩展(如 MySQL、PostgreSQL 或 SQLite 扩展)定期执行分析查询。
import duckdb
import psycopg2
# 连接到外部数据库
conn = psycopg2.connect(database="mydb", user="user", password="password", host="127.0.0.1", port="5432")
cur = conn.cursor()
cur.execute("INSERT INTO users VALUES (1, 'Alice')")
conn.commit()
cur.close()
conn.close()
# 使用 DuckDB 进行分析查询
con = duckdb.connect(database=':memory:')
con.execute("CREATE TABLE users AS SELECT * FROM read_parquet('path_to_parquet_file')")
result = con.execute("SELECT * FROM users").fetchall()
print(result)
4、写入 Parquet 文件:
- 将数据写入 Parquet 文件,然后使用 DuckDB 读取多个 Parquet 文件。
import duckdb
import pandas as pd
# 写入 Parquet 文件
df = pd.DataFrame({'id': [1], 'name': ['Alice']})
df.to_parquet('users.parquet')
# 使用 DuckDB 读取 Parquet 文件
con = duckdb.connect(database=':memory:')
con.execute("CREATE TABLE users AS SELECT * FROM read_parquet('users.parquet')")
result = con.execute("SELECT * FROM users").fetchall()
print(result)
5、Web 服务器:
- 创建一个 Web 服务器来接收请求并管理对 DuckDB 的读写操作。
from flask import Flask, request, jsonify
import duckdb
app = Flask(__name__)
con = duckdb.connect(database='my_database.duckdb')
@app.route('/insert', methods=['POST'])
def insert():
data = request.json
con.execute("INSERT INTO users VALUES (?, ?)", (data['id'], data['name']))
return jsonify({"status": "success"})
@app.route('/select', methods=['GET'])
def select():
result = con.execute("SELECT * FROM users").fetchall()
return jsonify(result)
if __name__ == '__main__':
app.run(debug=True)
乐观并发控制
DuckDB 使用乐观并发控制(Optimistic Concurrency Control),这种方法通常被认为是读密集型分析数据库系统的最佳选择,因为它可以加快读查询的处理速度。然而,任何在同一时间修改同一行的事务都会导致事务冲突错误:Transaction conflict: cannot update a table that has been altered!
总结
- 单进程并发:支持多线程写入,使用 MVCC 和乐观并发控制。
- 多进程并发:不支持自动多进程写入,需要应用层实现同步机制。
- 乐观并发控制:适用于读密集型分析查询,但写操作冲突时会抛出错误。