DuckDB 中的多数据库支持

Author Avatar
Mark Raasveldt
2024-01-26 · 6 分钟

TL;DR: 除了自身格式的数据库外,DuckDB 还可以附加 MySQL、Postgres 和 SQLite 数据库。这使得数据能够方便地读入 DuckDB 并在这些系统之间移动。

DuckDB supports reading and writing to MySQL, Postgres, and SQLite

在现代数据分析中,数据通常需要从各种不同的来源进行组合。数据可能存在于您机器上的 CSV 文件中,数据湖中的 Parquet 文件中,或在操作型数据库中。DuckDB 对在许多不同数据源之间移动数据提供了强大的支持。然而,此前这种支持仅限于读取数据和将数据写入文件。

DuckDB 支持对其原生存储格式进行高级操作——例如删除行、更新值或更改表的模式。它使用 ACID 语义支持所有这些操作。这保证了您的数据库始终处于一致状态——操作是原子性的,不会部分完成。

DuckDB 现在拥有一个可插拔的存储和事务层。这个灵活的层允许 DuckDB 扩展创建新的存储后端。这些存储后端可以支持所有数据库操作,就像 DuckDB 支持它们一样,包括插入数据甚至修改模式。

通过 MySQLPostgresSQLite 扩展实现这个新的可插拔存储和事务层,DuckDB 能够连接到这些系统并以与操作其原生存储引擎相同的方式对其进行操作。

这些扩展启用了许多有用的功能。例如,使用这些扩展您可以

  • 将数据从 SQLite 导出到 JSON
  • 将数据从 Parquet 读入 Postgres
  • 将数据从 MySQL 移动到 Postgres

……等等。

附加数据库

ATTACH 语句可用于向系统附加新数据库。默认情况下,将附加一个原生 DuckDB 文件。TYPE 参数可用于指定不同的存储类型。或者,也可以使用 {type}: 前缀。

例如,使用 SQLite 扩展,我们可以打开一个 SQLite 数据库文件,并像查询 DuckDB 数据库一样查询它。

ATTACH 'sakila.db' AS sakila (TYPE sqlite);
SELECT title, release_year, length FROM sakila.film LIMIT 5;
┌──────────────────┬──────────────┬────────┐
│      title       │ release_year │ length │
│     varchar      │   varchar    │ int64  │
├──────────────────┼──────────────┼────────┤
│ ACADEMY DINOSAUR │ 2006         │     86 │
│ ACE GOLDFINGER   │ 2006         │     48 │
│ ADAPTATION HOLES │ 2006         │     50 │
│ AFFAIR PREJUDICE │ 2006         │    117 │
│ AFRICAN EGG      │ 2006         │    130 │
└──────────────────┴──────────────┴────────┘

USE 命令切换主数据库。

USE sakila;
SELECT first_name, last_name FROM actor LIMIT 5;
┌────────────┬──────────────┐
│ first_name │  last_name   │
│  varchar   │   varchar    │
├────────────┼──────────────┤
│ PENELOPE   │ GUINESS      │
│ NICK       │ WAHLBERG     │
│ ED         │ CHASE        │
│ JENNIFER   │ DAVIS        │
│ JOHNNY     │ LOLLOBRIGIDA │
└────────────┴──────────────┘

SQLite 数据库可以像原生 DuckDB 数据库一样进行操作。例如,我们可以创建一个新表,用 Parquet 文件中的值填充它,从表中删除几行并更改表的模式。

CREATE TABLE lineitem AS FROM 'lineitem.parquet' LIMIT 1000;
DELETE FROM lineitem WHERE l_returnflag = 'N';
ALTER TABLE lineitem DROP COLUMN l_comment;

duckdb_databases 表包含所有附加数据库及其类型的列表。

SELECT database_name, path, type FROM duckdb_databases;
┌───────────────┬───────────┬─────────┐
│ database_name │   path    │  type   │
│    varchar    │  varchar  │ varchar │
├───────────────┼───────────┼─────────┤
│ sakila        │ sakila.db │ sqlite  │
│ memory        │ NULL      │ duckdb  │
└───────────────┴───────────┴─────────┘

混合使用

虽然附加不同类型的数据库很有用——但当结合使用时会变得更加强大。例如,我们可以同时附加 SQLite、MySQL 和 Postgres 数据库。

ATTACH 'sqlite:sakila.db' AS sqlite;
ATTACH 'postgres:dbname=postgresscanner' AS postgres;
ATTACH 'mysql:user=root database=mysqlscanner' AS mysql;

现在我们可以在这些附加的数据库之间移动数据并一起查询它们。让我们将 film 表复制到 MySQL,将 actor 表复制到 Postgres。

CREATE TABLE mysql.film AS FROM sqlite.film;
CREATE TABLE postgres.actor AS FROM sqlite.actor;

我们现在可以将这三个附加数据库中的表连接起来。让我们找出所有出演过 Ace Goldfinger 的演员。

SELECT first_name, last_name
FROM mysql.film
JOIN sqlite.film_actor ON (film.film_id = film_actor.film_id)
JOIN postgres.actor ON (actor.actor_id = film_actor.actor_id)
WHERE title = 'ACE GOLDFINGER';
┌────────────┬───────────┐
│ first_name │ last_name │
│  varchar   │  varchar  │
├────────────┼───────────┤
│ BOB        │ FAWCETT   │
│ MINNIE     │ ZELLWEGER │
│ SEAN       │ GUINESS   │
│ CHRIS      │ DEPP      │
└────────────┴───────────┘

对查询运行 EXPLAIN 会显示来自不同引擎的数据如何组合成最终查询结果。

┌───────────────────────────┐                                                          
│         PROJECTION        │                                                          
│   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   │                                                          
│         first_name        │                                                          
│         last_name         │                                                          
└─────────────┬─────────────┘                                                          
┌─────────────┴─────────────┐                                                          
│         HASH_JOIN         │                                                          
│   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   │                                                          
│           INNER           │                                                          
│     film_id = film_id     ├───────────────────────────────────────────┐              
└─────────────┬─────────────┘                                           │              
┌─────────────┴─────────────┐                             ┌─────────────┴─────────────┐
│         HASH_JOIN         │                             │           FILTER          │
│   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   │                             │   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   │
│           INNER           │                             │ (title = 'ACE GOLDFINGER')│
│    actor_id = actor_id    ├──────────────┐              │                           │
└─────────────┬─────────────┘              │              └─────────────┬─────────────┘
┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐
│        SQLITE_SCAN        ││       POSTGRES_SCAN       ││        MYSQL_SCAN         │
│   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   ││   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   ││   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   │
│    sakila.db:film_actor   ││           actor           ││            film           │
│   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   ││   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   ││   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   │
│          film_id          ││          actor_id         ││          film_id          │
│          actor_id         ││         first_name        ││           title           │
│                           ││         last_name         ││                           │
└───────────────────────────┘└───────────────────────────┘└───────────────────────────┘

事务

在 DuckDB 中执行的所有语句都在事务中执行。如果未显式调用 BEGIN TRANSACTION,则每个语句都将在其自己的事务中执行。这同样适用于在其他存储引擎上执行的查询。这些存储引擎也支持显式的 BEGINCOMMITROLLBACK 语句。

例如,我们可以在附加的 SQLite 数据库中开始一个事务,进行更改,然后回滚。原始数据将被恢复。

BEGIN;
TRUNCATE film;
SELECT title, release_year, length FROM film;
┌─────────┬──────────────┬────────┐
│  title  │ release_year │ length │
│ varchar │   varchar    │ int64  │
├─────────────────────────────────┤
│             0 rows              │
└─────────────────────────────────┘
ROLLBACK;
SELECT title, release_year, length FROM film LIMIT 5;
┌──────────────────┬──────────────┬────────┐
│      title       │ release_year │ length │
│     varchar      │   varchar    │ int64  │
├──────────────────┼──────────────┼────────┤
│ ACADEMY DINOSAUR │ 2006         │     86 │
│ ACE GOLDFINGER   │ 2006         │     48 │
│ ADAPTATION HOLES │ 2006         │     50 │
│ AFFAIR PREJUDICE │ 2006         │    117 │
│ AFRICAN EGG      │ 2006         │    130 │
└──────────────────┴──────────────┴────────┘

多数据库事务

每个存储引擎都有其独立的事务,由存储引擎本身管理。例如,在 Postgres 中打开一个事务,会在 Postgres 客户端中调用 BEGIN TRANSACTION。该事务由 Postgres 本身管理。同样,当事务提交或回滚时,存储引擎会自行处理。

事务用于**读取**和**写入**数据。对于读取数据,它们用于提供数据库的一致快照。对于写入数据,它们用于确保事务中的所有数据打包在一起并同时写入。

当执行涉及多个附加数据库的事务时,我们需要打开多个事务:事务中使用的每个附加数据库一个事务。虽然这在从数据库**读取**时不是问题,但在**写入**时会变得复杂。特别是,当我们想要 COMMIT 一个事务时,很难确保 (a) 每个数据库都已成功提交,或者 (b) 每个数据库都已回滚。

因此,目前不支持在单个事务中**写入**多个附加数据库。相反,当尝试这样做时会抛出错误

BEGIN;
CREATE TABLE postgres.new_table (i INTEGER);
CREATE TABLE mysql.new_table (i INTEGER);
Error: Attempting to write to database "mysql" in a transaction that has
already modified database "postgres" – a single transaction can only write
to a single attached database.

在数据库之间复制数据

CREATE TABLE ASINSERT INTOCOPY 可用于在不同附加数据库之间复制数据。专用的 COPY FROM DATABASE ... TO 可用于将所有数据从一个数据库复制到另一个数据库。这包括存储在源数据库中的所有表和视图。

-- attach a Postgres database
ATTACH 'postgres:dbname=postgresscanner' AS postgres;
-- attach a DuckDB file
ATTACH 'database.db' AS ddb;
-- export all tables and views from the Postgres database to the DuckDB file
COPY FROM DATABASE postgres TO ddb;

直接打开数据库

连接到不同数据库类型不需要显式的 ATTACH 语句。实例化 DuckDB 实例时,可以使用 {type}: 前缀直接连接到不同的数据库类型。例如,要连接到 SQLite 文件,请使用 sqlite:file.db。要连接到 Postgres 实例,请使用 postgres:dbname=postgresscanner。这可以在任何客户端中完成,包括 CLI。例如

CLI

duckdb sqlite:file.db

Python

import duckdb
con = duckdb.connect('sqlite:file.db')

这相当于附加存储引擎并在之后运行 USE

结论

DuckDB 的可插拔存储引擎架构支持许多用例。通过附加多个数据库,可以以事务安全的方式提取数据,用于批量 ETL 或 ELT 工作负载,以及实时数据虚拟化工作负载。这些技术也可以很好地结合使用,例如,定期批量移动数据,同时实时填充最后的少量数据点。

可插拔存储引擎还开辟了在数据平台中处理并发写入器的新方法。每个独立的进程可以将其输出写入事务型数据库,并且结果可以在 DuckDB 中组合——所有这些都以事务安全的方式进行。然后,数据分析任务可以在集中的 DuckDB 数据库上进行,以提高性能。

我们期待听到您使用此功能的许多创新方式!

未来工作

我们打算继续增强现有扩展的性能和功能。此外,所有这些功能都可以由社区利用,以连接到其他数据库。