Использование pg_duckdb

Обзор

ADP/PostgreSQL хорошо справляется с транзакционной нагрузкой (OLTP), но уступает специализированным решениям при обработке тяжелых аналитических запросов (OLAP). Расширение pg_duckdb интегрирует колоночно-векторизованный аналитический движок DuckDB в ADP/PostgreSQL, обеспечивая высокопроизводительную аналитику и работу с ресурсоемкими приложениями. Это позволяет выполнять аналитические SQL-запросы на данных ADP/PostgreSQL с высокой производительностью, не переписывая код и не экспортируя данные в отдельный формат. Расширение доступно в Enterprise-версии ADP.

Расширение pg_duckdb включает набор функций, которые позволяют читать внешние файлы (Parquet, CSV, JSON), работать с озерами данных (Iceberg, Delta), управлять кешем DuckDB и секретами, а также использовать нативные SQL‑функции DuckDB напрямую из ADP/PostgreSQL. В этой статье приведено несколько примеров использования функций pg_duckdb. Для получения полного и актуального списка функций обратитесь к официальной документации: Functions.

Установка

Пакет, требуемый для установки pg_duckdb, поставляется с ADP. Чтобы использовать pg_duckdb, выполните следующие шаги:

  1. На вкладке Primary Configuration сервиса ADPG добавьте pg_duckdb к значению параметра shared_preload_libraries поля postgresql.conf раздела ADPG configurations (см. Настройка сервисов).

    Установка параметра shared_preload_libraries
    Установка параметра "shared_preload_libraries"

    После изменения postgresql.conf нажмите Save и выполните действие Reconfigure & Restart, чтобы применить изменения.

  2. Выполните команду CREATE EXTENSION в той базе данных, в которую вы хотите установить расширение (можно использовать psql или любую другую утилиту для подключения к базе данных):

    CREATE EXTENSION pg_duckdb;
ПРИМЕЧАНИЕ
Если расширение pg_duckdb создано в базе данных template1, используемой в качестве шаблона по умолчанию, во всех вновь создаваемых базах данных будет установлено это расширение.

ADP использует версию 1.1.0 расширения pg_duckdb. Чтобы это проверить, можно выполнить следующий запрос:

SELECT extversion FROM pg_extension
    WHERE extname = 'pg_duckdb';
 extversion
------------
1.1.0

После создания расширения pg_duckdb его функции становятся доступны в текущей базе данных. Достаточно установить параметр duckdb.force_execution в значение true для использования SQL-движка DuckDB при выполнении запросов:

SET duckdb.force_execution = true;

Поддерживаемые типы

Расширение pg_duckdb поддерживает следующие типы данных для использования в запросах:

  • целочисленные типы (integer, bigint и другие);

  • числа с плавающей точкой (real, double precision);

  • numeric (может быть преобразован в double precision, если pg_duckdb не поддерживает требуемую точность);

  • text, varchar, bpchar;

  • типы bit, включая как массивы битов фиксированного, так и переменного размера;

  • bytea, blob;

  • timestamp, timestamptz, date, interval, timestamp_ns, timestamp_ms, timestamp_s;

  • boolean;

  • uuid;

  • json, jsonb;

  • domain;

  • массивы для всех вышеперечисленных типов, с некоторыми ограничениями для многомерных массивов (см. Known limitations).

Специальные типы

Расширение pg_duckdb вводит несколько специальных типов PostgreSQL. Вам не следует использовать эти типы явно, но они могут упоминаться в сообщениях об ошибках ADP/PostgreSQL.

duckdb.row

Тип duckdb.row возвращается функциями read_parquet, read_csv, scan_iceberg и им подобными. Такие функции могут возвращать строки с разными столбцами и типами в зависимости от их аргументов. Укажите имя столбца в квадратных скобках, чтобы получить значение конкретного столбца:

SELECT t['name'], t['description'] FROM read_parquet('parquet_file') t WHERE t['price'] < 100;

Если вы используете выражение SELECT *, результат запроса не будет содержать столбец с типом duckdb.row. Все столбцы получат свои реальные типы:

SELECT * FROM read_parquet('parquet_file');

duckdb.unresolved_type

Расширение pg_duckdb использует тип duckdb.unresolved_type, чтобы ADP/PostgreSQL мог понять выражение, тип которого неизвестен на этапе разбора запроса. После того как pg_duckdb выполнит запрос, фактический тип будет заполнен движком DuckDB. Таким образом, результат запроса никогда не будет содержать столбец с типом duckdb.unresolved_type.

Вы можете получить ошибки, сообщающие о том, что функция или оператор не существует для duckdb.unresolved_type. Например:

ERROR: function my_function(duckdb.unresolved_type) does not exist
LINE 13: my_function(t['column1']) as column1

В этом случае явно приведите аргумент к типу, который принимает функция: my_function(t['column1']::text) as column1.

Если необходимо вставить результат выражения SELECT в таблицу (команда INSERT INTO), явно приведите к необходимому типу столбец, извлеченный с помощью синтаксиса t['column_name']:

INSERT INTO table1 SELECT t['height']::float FROM read_csv('buildings.csv') t;

Выражение, возвращаемое синтаксисом t['column_name'], имеет тип duckdb.unresolved_type. Его фактический тип становится известен только после того, как pg_duckdb выполнит запрос, но ADP/PostgreSQL необходимо знать тип при разборе запроса INSERT — без приведения типа вы получите ошибку.

duckdb.json

Расширение pg_duckdb использует duckdb.json в качестве аргументов JSON-функций pg_duckdb. Тип duckdb.json позволяет этим функциям принимать значения JSON, JSONB и duckdb.unresolved_type.

За более подробной информацией о типах, которые поддерживает pg_duckdb, и об ограничениях их использования обратитесь к статье Types.

Транзакции в pg_duckdb

Расширение pg_duckdb поддерживает транзакции с несколькими операциями, но имеет важное ограничение — не рекомендуется выполнять запись одновременно в таблицу ADP/PostgreSQL и таблицу DuckDB в рамках одной транзакции. Вы можете выполнять DDL-операции (например, CREATE TABLE, DROP TABLE) с таблицами DuckDB внутри транзакции, но не допускается объединять такие операторы с DDL-операциями, затрагивающими объекты ADP/PostgreSQL.

Чтобы отключить это ограничение и разрешить запись как в DuckDB, так и в ADP/PostgreSQL в рамках одной транзакции, установите для параметра duckdb.unsafe_allow_mixed_transactions значение true. Это может стать причиной того, что транзакция будет зафиксирована только в DuckDB, но не в ADP/PostgreSQL, что приведет к несогласованности и потере данных. Например, следующий код может привести к удалению таблицы duckdb_table без копирования ее содержимого в pg_table:

BEGIN;
SET LOCAL duckdb.unsafe_allow_mixed_transactions TO true;
CREATE TABLE pg_table AS SELECT * FROM duckdb_table;
DROP TABLE duckdb_table;
COMMIT;

Предустановленные расширения DuckDB

pg_duckdb поддерживает большое количество расширений DuckDB, которые дополняют функциональность pg_duckdb для различных сценариев использования. По умолчанию предустановлены и загружены следующие расширения:

  • httpfs — реализует поддержку файловой системы HTTP/S3, позволяя читать и записывать файлы в удаленных хранилищах;

  • json — позволяет использовать функции и операторы для работы с JSON.

Примеры

Раздел содержит несколько примеров, иллюстрирующих часто используемые сценарии работы с pg_duckdb.

Использование pg_duckdb для запросов к таблицам ADP/PostgreSQL

Создайте таблицу и заполните ее данными:

CREATE TABLE books (
    id SERIAL PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    genre VARCHAR(50),
    price NUMERIC(10, 2),
    total_sales BIGINT);

INSERT INTO books (title, genre, price, total_sales)
VALUES
    ('Mrs. Dalloway', 'novel', 360, 6212880),
    ('To the Lighthouse', 'novel', 440, 7216000),
    ('To Kill a Mockingbird', 'novel', 750, 11574000),
    ('The Great Gatsby', 'novel', 900, 11110500),
    ('The Lord of the Rings', 'fantasy', 1200, 5472000),
    ('1984', 'sci-fi', 520, 9642880),
    ('The Hobbit, or There and Back Again', 'fantasy', 1100, 19679000),
    ('War and Peace', 'novel', 1500, 32548500),
    ('Hyperion', 'sci-fi', 610, 8411290),
    ('The Time Machine', 'sci-fi', 450, 6444450);

SET duckdb.force_execution = true;

Выполните команду EXPLAIN ANALYZE, чтобы отобразить план выполнения запроса:

EXPLAIN ANALYZE SELECT genre, AVG(price) AS average_price, COUNT(*) AS books_count
    FROM books GROUP BY genre ORDER BY average_price DESC;

В выводе видно, что запрос выполнен движком DuckDB:

                                                QUERY PLAN
-----------------------------------------------------------------------------------------------------------------
 Custom Scan (DuckDBScan) (cost=0.00..0.00 rows=0 width=0) (actual time=0.001..0.002 rows=0 loops=1)
   DuckDB Execution Plan:

 ┌─────────────────────────────────────┐
 │┌───────────────────────────────────┐│
 ││    Query Profiling Information    ││
 │└───────────────────────────────────┘│
 └─────────────────────────────────────┘
 EXPLAIN ANALYZE SELECT genre, avg(price) AS average_price, count(*) AS books_count FROM pgduckdb.public.books
    GROUP BY genre ORDER BY (avg(price)) DESC
 ┌────────────────────────────────────────────────┐
 │┌──────────────────────────────────────────────┐│
 ││              Total Time: 0.0093s             ││
 │└──────────────────────────────────────────────┘│
 └────────────────────────────────────────────────┘
 ┌───────────────────────────┐
 │           QUERY           │
 └─────────────┬─────────────┘
 ┌─────────────┴─────────────┐
 │      EXPLAIN_ANALYZE      │
 │    ────────────────────   │
 │           0 rows          │
 │          (0.00s)          │
 └─────────────┬─────────────┘
 ┌─────────────┴─────────────┐
 │          ORDER_BY         │
 │    ────────────────────   │
 │   avg(books.price) DESC   │
 │                           │
 │           3 rows          │
 │          (0.00s)          │
 └─────────────┬─────────────┘
 ┌─────────────┴─────────────┐
 │       HASH_GROUP_BY       │
 │    ────────────────────   │
 │         Groups: #0        │
 │                           │
 │        Aggregates:        │
 │          avg(#1)          │
 │        count_star()       │
 │                           │
 │           3 rows          │
 │          (0.00s)          │
 └─────────────┬─────────────┘
 ┌─────────────┴─────────────┐
 │         PROJECTION        │
 │    ────────────────────   │
 │           genre           │
 │           price           │
 │                           │
 │          10 rows          │
 │          (0.00s)          │
 └─────────────┬─────────────┘
 ┌─────────────┴─────────────┐
 │         TABLE_SCAN        │
 │    ────────────────────   │
 │        Table: books       │
 │                           │
 │        Projections:       │
 │           genre           │
 │           price           │
 │                           │
 │          10 rows          │
 │          (0.01s)          │
 └───────────────────────────┘

 Planning Time: 0.521 ms
 Execution Time: 0.421 ms

Выполните запрос и просмотрите результат, чтобы убедиться в корректной работе движка DuckDB:

SELECT genre, AVG(price) AS average_price, COUNT(*) AS books_count
FROM books GROUP BY genre ORDER BY average_price DESC;
  genre  |   average_price   | books_count
---------+-------------------+-------------
 fantasy |              1150 |           2
 novel   |               790 |           5
 sci-fi  | 526.6666666666666 |           3

Чтение данных из csv-файла

Расширение pg_duckdb содержит функцию read_csv, позволяющую получать данные из CSV-файлов.

Например, следующий файл CSV находится по пути tmp/books.csv:

id,title,author_id,public_year,genre
1,Mrs. Dalloway,1,1925,novel
2,To the Lighthouse,1,1927,novel
3,To Kill a Mockingbird,2,1960,novel
4,The Lord of the Rings,4,1955,fantasy
5,1984,5,1949,sci-fi

Выполните запрос ниже, чтобы вывести значения полей title и public_year:

SELECT t['title'], t['public_year']
     FROM read_csv('file:///tmp/books.csv') t;
         title         | public_year
-----------------------+-------------
 Mrs. Dalloway         |        1925
 To the Lighthouse     |        1927
 To Kill a Mockingbird |        1960
 The Lord of the Rings |        1955
 1984                  |        1949

Можно в одном запросе объединять данные из CSV-файлов и из таблиц базы данных. Создайте таблицу authors для демонстрации этой функциональности:

CREATE TABLE authors (
    id SERIAL PRIMARY KEY,
    author_name VARCHAR(100) NOT NULL,
    country VARCHAR(20)
);

INSERT INTO authors (author_name, country) VALUES
    ('Virginia Woolf', 'Great Britain'),
    ('Harper Lee', 'USA'),
    ('F. Scott Fitzgerald', 'USA'),
    ('J.R.R. Tolkien', 'Great Britain'),
    ('George Orwell', 'Great Britain');

Следующий запрос выводит список книг из файла books.csv и имена их авторов из таблицы authors:

SELECT t1['title'], t1['public_year'], t2.author_name FROM read_csv('file:///tmp/books.csv') t1
    INNER JOIN authors t2
    ON t1['author_id'] = t2.id;
         title         | public_year |  author_name
-----------------------+-------------+----------------
 To the Lighthouse     |        1927 | Virginia Woolf
 To Kill a Mockingbird |        1960 | Harper Lee
 The Lord of the Rings |        1955 | J.R.R. Tolkien
 1984                  |        1949 | George Orwell
 Mrs. Dalloway         |        1925 | Virginia Woolf

Функция read_csv также позволяет получать данные CSV-файлов из S3-хранилищ. Для этого необходимо создать секрет, как показано в примере Чтение файлов из S3-хранилища.

Чтение данных из JSON-файла

Функция read_json позволяет получать данные из JSON-файлов.

Например, имеется JSON-файл по пути tmp/orders.json:

[{"customer": "Jacob Johnson",
  "book": "Hyperion",
  "qty": 3
},
{"customer": "Adam Brown",
  "book": "War and Peace",
  "qty": 2
},

{"customer": "Andrew Nelson",
  "book": "1984",
  "qty": 4
}]

Вызовите функцию read_json, передав в качестве параметра путь к файлу:

SELECT * FROM read_json('file:///tmp/orders.json');
   customer    |     book      | qty
---------------+---------------+-----
 Jacob Johnson | Hyperion      |   3
 Adam Brown    | War and Peace |   2
 Andrew Nelson | 1984          |   4

Функция read_json также позволяет получать данные JSON-файлов из S3-хранилищ. Для этого необходимо создать секрет, как показано в примере Чтение файлов из S3-хранилища.

Операции с файлами Parquet

Расширение pg_duckdb позволяет выполнять запросы над файлами типа Parquet, а также экспортировать данные в этот формат.

Экспорт данных из таблицы ADP/PostgreSQL в формат Parquet

pg_duckdb может экспортировать данные таблиц ADP/PostgreSQL в файлы типа Parquet, находящиеся в разных хранилищах, в том числе облаках, озерах данных и локальном диске компьютера. Код ниже создает файл Parquet с книгами жанра novel на локальном диске по следующему пути: tmp/output_file.parquet:

COPY (SELECT * FROM books WHERE genre = 'novel')
    TO 'file:///tmp/output_file.parquet'
    (FORMAT 'parquet');

Выполнение запросов над файлами типа Parquet

Код ниже использует функцию read_parquet, чтобы отобразить содержимое файла output_file.parquet, созданного в предыдущем примере (t — псевдоним для извлекаемой таблицы):

SELECT t['title'], t['price']
     FROM read_parquet('file:///tmp/output_file.parquet') t;
         title         |  price
-----------------------+---------
 Mrs. Dalloway         |  360.00
 To the Lighthouse     |  440.00
 To Kill a Mockingbird |  750.00
 The Great Gatsby      |  900.00
 War and Peace         | 1500.00

Объединение данных ADP/PostgreSQL и Parquet в одном запросе

Вы можете обращаться к таблицам ADP/PostgreSQL и внешним источникам данных в рамках одного запроса. Следующий код выводит строки из таблицы books, которых нет в файле Parquet:

SELECT t1.* FROM books t1
    LEFT JOIN read_parquet('file:///tmp/output_file.parquet') t2 ON t1.id = t2['id']
    WHERE t2['id'] IS NULL;
 id |                title                |  genre  |  price  | total_sales
----+-------------------------------------+---------+---------+-------------
  5 | The Lord of the Rings               | fantasy | 1200.00 |     5472000
  6 | 1984                                | sci-fi  |  520.00 |     9642880
  7 | The Hobbit, or There and Back Again | fantasy | 1100.00 |    19679000
  9 | Hyperion                            | sci-fi  |  610.00 |     8411290
 10 | The Time Machine                    | sci-fi  |  450.00 |     6444450

Чтение файлов из S3-хранилища

В DuckDB хранение учетных данных (таких как ключи доступа, токены, пароли и прочее) реализовано через секреты. Секреты позволяют базе данных подключаться к защищенным внешним хранилищам или удаленным серверам без указания ключей в каждом запросе.

Чтобы подключиться к S3-хранилищу, сначала создайте секрет, используя функцию duckdb.create_simple_secret:

SELECT duckdb.create_simple_secret(
      type      := 'S3',
      key_id    := 'Access_key',
      secret    := 'Secret_key',
      region    := 'ru-central1',
      endpoint  := 'storage.yandexcloud.net',
      use_ssl   := 'false'
);

S3-хранилище содержит файл test.parquet, в котором есть столбцы PassengerId и Name. Выполните следующий запрос, чтобы вывести данные из этого файла:

SELECT t['PassengerId'] AS id, t['Name'] AS name
    FROM read_parquet('s3://test-bucket/test/test.parquet') t;

где test-bucket — название бакета, test/test.parquet — путь к файлу в бакете.

 id  |       name
-----+---------------------
 1   | Jacques Heath
 2   | Timothy McCarthy
 3   | Laina Heikkinen
 4   | Jacques Futrelle
 5   | William Allen
 6   | James Moran
 7   | Elisabeth Walton
 8   | Kornelia Theodosia
 9   | Oscar Johnson
 10  | Madeleine Talmage

Преобразование CSV-файла в файл формата Parquet

pg_duckdb позволяет конвертировать CSV-файлы в формат Parquet, не создавая PostgreSQL-таблиц. Сконвертируйте файл books.csv, приведенный выше, в Parquet-файл, используя следующий запрос:

COPY (SELECT t['id']::integer,
             t['title']::text,
             t['author_id']::integer,
             t['public_year']::integer,
             t['genre']::text FROM read_csv('file:///tmp/books.csv') t)
    TO 'file:///tmp/books.parquet' (FORMAT 'parquet');

Обратите внимание, что для успешной конвертации необходимо явно указать типы столбцов. В противном случае парсер ADP/PostgreSQL не сможет разобрать данные для передачи в команду COPY, так как функция read_csv возвращает тип duckdb.row.

Создание высокопроизводительных временных таблиц

Расширение pg_duckdb позволяет создавать высокопроизводительные временные таблицы с помощью конструкции USING duckdb. При таком подходе выполнение операций и хранение данных временной таблицы осуществляются в колоночном движке DuckDB, работающем в оперативной памяти, что позволяет полностью обойти более медленные механизмы PostgreSQL — строковое хранилище (heap) и журнал предзаписи (WAL).

Конструкция USING duckdb для постоянных таблиц заблокирована на локальном уровне и требует интеграции с MotherDuck. Однако для временных таблиц ограничений нет: они создаются напрямую внутри движка DuckDB.

Создайте временную таблицу, используя данные из таблицы books:

CREATE TEMP TABLE temp_books USING duckdb AS
    SELECT id, title, genre, price, total_sales
    FROM books;

Выполните команду \dt+:

\dt+
                                         List of relations
  Schema   |    Name    | Type  |  Owner   | Persistence | Access method |    Size    | Description
-----------+------------+-------+----------+-------------+---------------+------------+-------------
 pg_temp_6 | temp_books | table | postgres | temporary   | duckdb        | 0 bytes    |
 public    | books      | table | postgres | permanent   | heap          | 8192 bytes |

Вы можете увидеть в выводе, что методом доступа к таблице temp_books является duckdb.

Любые операции агрегирования, группировки и сканирования для временной таблицы, созданной с использованием USING duckdb, будут выполняться движком DuckDB со скоростью, характерной для OLAP-баз данных.

Нашли ошибку? Выделите текст и нажмите Ctrl+Enter чтобы сообщить о ней