httpfs: чтение S3, GCS, Azure и HTTP
В прошлых уроках мы читали файлы с локального диска. Но в реальной аналитике данные живут в объектном хранилище — S3, Google Cloud Storage, Azure Blob. Расширение httpfs стирает эту границу: после его подключения путь s3://bucket/data/file.parquet работает в read_parquet ровно так же, как локальный путь. Все оптимизации предыдущего модуля — projection pushdown, filter pushdown, partition pruning, glob-шаблоны — продолжают действовать поверх объектного хранилища. Этот урок про то, как httpfs устроен и как безопасно передать ему учётные данные через механизм secrets.
httpfs как виртуальная файловая система
Trino: Lakehouse-архитектура и object storageКлючевая идея httpfs: он регистрирует в DuckDB новые файловые системы. Ядро DuckDB работает с файлами через абстракцию file system — у неё есть операции «открыть», «прочитать диапазон байт», «узнать размер». Локальная файловая система — одна реализация этой абстракции. httpfs добавляет ещё несколько: для протокола s3://, для gcs://, для azure:// (азурную часть фактически обслуживает расширение azure, тесно связанное с httpfs), для http:// и https://.
Сканер Parquet не знает и не должен знать, откуда берутся байты. Он просит файловую систему «дай мне байты с 1000 по 5000» — а уж локальная это файловая система или S3-реализация httpfs, делающая HTTP-запрос с заголовком Range, ему безразлично. Поэтому весь механизм pushdown работает поверх S3 без изменений: DuckDB по footer вычисляет нужные байтовые диапазоны и запрашивает только их HTTP Range-запросами.
httpfs — core-расширение и autoloadable: первое обращение к s3://-пути подтянет его само. Явный INSTALL httpfs; LOAD httpfs; нужен редко, но знать про него полезно для офлайн-окружений.
-- httpfs подтянется автоматически при обращении к s3://
SELECT COUNT(*) FROM 's3://my-bucket/events/2026/*.parquet';
-- Чтение публичного файла прямо по HTTP — тоже httpfs
SELECT * FROM read_csv('https://example.com/data.csv') LIMIT 5;
Range-запросы: почему чтение с S3 не так дорого, как кажется
Может показаться, что запрос к 10-гигабайтному Parquet на S3 обязан скачать все 10 ГБ. Это не так — и причина в том, что HTTP поддерживает заголовок Range, позволяющий запросить произвольный кусок файла, а не файл целиком.
DuckDB этим активно пользуется. Чтение Parquet с S3 идёт по шагам: сначала маленький Range-запрос на footer (метаданные в конце файла), из footer DuckDB узнаёт схему, расположение row groups и их статистику; затем — projection pushdown отбрасывает ненужные колонки, filter pushdown по статистике отбрасывает ненужные row groups; и только потом DuckDB делает Range-запросы ровно на те байтовые диапазоны, где лежат нужные column chunk-и нужных row groups. Из 10 ГБ по сети может реально прийти несколько сотен мегабайт.
Поэтому правило из модуля про внешние данные на S3 действует с удвоенной силой: не пишите SELECT *, фильтруйте по кластеризованным колонкам. На локальном диске лишнее чтение стоит времени; на S3 оно стоит времени, трафика и денег за исходящий трафик.
CSV и JSON на S3 в этом смысле хуже Parquet. У них нет footer, и filter pushdown по row groups невозможен — чтобы что-то посчитать, файл приходится во многом прочитать целиком по сети. Если данные большие и лежат в облаке, держите их в Parquet, а не в CSV: разница в объёме скачиваемого по сети будет в разы.
Проблема учётных данных
Приватный бакет требует аутентификации: ключ доступа и секретный ключ для AWS, ключ аккаунта для Azure, и так далее. Возникает вопрос — как передать эти данные DuckDB безопасно. Соблазнительный, но плохой путь — вписать ключи прямо в текст запроса или в конфигурацию. Тогда секреты утекут в логи запросов, в историю команд, в текст сохранённого SQL-файла. Это типичная утечка учётных данных.
DuckDB решает проблему механизмом secrets — отдельным защищённым хранилищем учётных данных внутри DuckDB. Вы один раз создаёте secret, а в запросах ключи больше не упоминаете вообще: при обращении к s3:// DuckDB сам находит подходящий secret и берёт данные из него.
CREATE SECRET: правильный способ
Secret создаётся командой CREATE SECRET. У него есть тип (S3, GCS, AZURE и другие) и провайдер — способ, которым secret получает значения.
Провайдер CONFIG — значения задаются явно в команде:
CREATE SECRET my_s3 (
TYPE s3,
KEY_ID 'AKIA...',
SECRET 'wJalr...',
REGION 'eu-central-1'
);
Провайдер CREDENTIAL_CHAIN — значения не пишутся вообще; DuckDB берёт их из стандартной цепочки источников AWS: переменные окружения, файл ~/.aws/credentials, IAM-роль EC2-инстанса или контейнера. Это предпочтительный способ в продакшене: в SQL-коде нет ни одного ключа.
-- Никаких ключей в коде: данные берутся из окружения / IAM-роли
CREATE SECRET my_s3 (
TYPE s3,
PROVIDER credential_chain
);
После создания secret запросы к s3:// просто работают — ключи в них не нужны:
CREATE SECRET my_s3 (TYPE s3, PROVIDER credential_chain);
-- Secret подхватывается автоматически, в запросе ключей нет
SELECT carrier, COUNT(*) AS n
FROM 's3://my-private-bucket/flights/*.parquet'
GROUP BY carrier;
Scope: несколько secrets для разных хранилищ
В одном проекте часто несколько бакетов в разных аккаунтах или облаках. Чтобы DuckDB понимал, какой secret к какому пути применять, у secret есть scope — префикс пути, на который он распространяется.
-- Secret для конкретного бакета
CREATE SECRET prod_data (
TYPE s3,
PROVIDER credential_chain,
SCOPE 's3://prod-bucket'
);
-- Другой secret для другого бакета
CREATE SECRET analytics_data (
TYPE s3,
PROVIDER credential_chain,
SCOPE 's3://analytics-bucket'
);
При запросе DuckDB выбирает secret с самым длинным совпадающим scope-префиксом. Запрос к s3://prod-bucket/... возьмёт prod_data, к s3://analytics-bucket/... — analytics_data. Это та же логика «самое специфичное правило побеждает», что и при выборе таблицы каталогом.
По умолчанию secret временный — живёт только в текущей сессии. CREATE PERSISTENT SECRET сохраняет secret на диск (в зашифрованном виде), и он переживает перезапуск DuckDB. Посмотреть все secrets можно через SELECT * FROM duckdb_secrets() — в выводе сами секретные значения скрыты.
| Свойство | Временный secret | PERSISTENT SECRET |
|---|---|---|
| Где хранится | В памяти сессии | На диске (зашифрован) |
| Переживает перезапуск | Нет | Да |
| Когда применять | Разовые задачи, ноутбук | Постоянный пайплайн, сервер |
S3-совместимых хранилищ много — MinIO, Cloudflare R2, Backblaze B2 — и все они говорят на протоколе S3, но по другому адресу (endpoint). Для них в CREATE SECRET нужно явно задать ENDPOINT и обычно URL_STYLE ‘path’. Без указания endpoint DuckDB по умолчанию обратится к настоящему AWS S3 и получит ошибку аутентификации, которая выглядит загадочно, пока не вспомнишь про endpoint. Параметр USE_SSL управляет тем, идёт ли соединение по HTTPS.
Попробуй сам
Для этого задания удобно поднять локальный MinIO (S3-совместимое хранилище в Docker) — тогда не нужен реальный AWS-аккаунт.
- Прочитайте публичный файл по HTTP без всякой настройки:
SELECT * FROM read_csv('https://...public.csv') LIMIT 10. Убедитесь, что httpfs подтянулся автоматически — проверьте черезduckdb_extensions(). - Поднимите MinIO, создайте бакет, положите в него Parquet-файл. Создайте
CREATE SECRETтипа s3 с провайдером CONFIG, обязательно указав ENDPOINT MinIO и URL_STYLE ‘path’. Прочитайте файл по путиs3://bucket/file.parquet. - Выполните
SELECT * FROM duckdb_secrets()и убедитесь, что secret виден, но его секретное значение в выводе скрыто. - Создайте два secrets с разными SCOPE и объясните себе, по какому правилу DuckDB выберет нужный при запросе к конкретному бакету. Сравните временный secret и PERSISTENT SECRET: какой переживёт перезапуск процесса.
Parquet поверх объектного хранилища: HTTP range requests и оптимизации