ИИгрушки 🤖
Сегодня еще кстати крылатое выражение на уме или цитата, как хотите. «Когда выручка не растет, кровати 🛌 передвинуты, ш..х сменили и все против вас, то на помощь приходят ИИгрушки) 😁☺️😉 (с)
Welcome to my personal place for love, peace and happiness 🤖
Сегодня еще кстати крылатое выражение на уме или цитата, как хотите. «Когда выручка не растет, кровати 🛌 передвинуты, ш..х сменили и все против вас, то на помощь приходят ИИгрушки) 😁☺️😉 (с)
В первой части Утиных историй мы детально разбирали, как DuckDB переворачивает принципы локальной и встраиваемой аналитики. Сегодня на календаре 19 апреля 2026 года, и экосистема «утки» развивается с невероятной скоростью. На днях вышел юбилейный, 40-й выпуск информационного бюллетеня от команды MotherDuck.
В этой статье мы разберем самые горячие новинки обновления: релиз DuckLake 1.0, нативную поддержку протокола PostgreSQL, векторный поиск и то, как DuckDB покоряет новые горизонты программирования (от Elixir к Rust).
Главная новость апреля — релиз DuckLake 1.0. Это lakehouse-формат, в котором все метаданные хранятся непосредственно в каталоге базы данных (в PostgreSQL, SQLite или самой DuckDB), а не в разрозненных файлах, как это сделано в Delta Lake или Apache Iceberg.
Отказ от чтения разрозненных файлов метаданных дает феноменальный прирост производительности базовых операций агрегации. Если сравнивать время выполнения запросов до оптимизации (T old) и с использованием чтения исключительного из каталога метаданных DuckLake (T new), то выигрыш в скорости можно выразить формулой:
Speedup =T new / T old
Для запросов вида `COUNT(*)` этот Speedup составляет от 8 до 258 раз! А вызов системной функции `duckdb_views()` ускорился примерно в 70 раз.
Неудивительно, что DuckLake уже входит в топ-10 расширений по количеству скачиваний и поддерживается клиентами Apache DataFusion, Spark, Trino и Pandas. Издательство O’Reilly даже готовит книгу *“DuckLake: The Definitive Guide”*. (Фича доступна в DuckDB v1.5.2).
Чтобы внедрить мощь DuckDB в свою инфраструктуру разработчикам часто приходилось искать специальные драйверы и коннекторы. Это в прошлом!
MotherDuck запустили PostgreSQL wire-protocol endpoint. Теперь вы можете выполнять аналитические SQL-запросы к DuckDB, используя совершенно любой клиент, пулер (pooler) или BI-инструмент, совместимый с Postgres. Устанавливать библиотеки DuckDB на клиент больше не нужно!
Достаточно направить ваш текущий клиент по адресу:
pg.us-east-1-aws.motherduck.com:5432Авторизация происходит с помощью токена MotherDuck. При этом диалект SQL остается утиным (хотя он в значительной степени и совместим с PostgreSQL). Миграция данных возможна через обычные ETL-утилиты или расширение `pg_duckdb`.
Мощным толчком для развития комьюнити-плагинов стал релиз `quack-rs`. До сих пор написание расширений для DuckDB на Rust требовало создания слоев совместимости (C++ glue) и возни с CMake.
`quack-rs` — это SDK на чистом Rust, который оборачивает *C Extension API* (v1.1+). Инструмент предоставляет безопасные абстракции и устраняет 16 задокументированных проблем с FFI (Foreign Function Interface), предотвращая “тихую” порчу данных через NULL и ошибки “double-free” в callback-функциях агрегации.
Для старта нового расширения достаточно вызвать функцию:
generate_scaffold();Она сгенерирует все 11 файлов, необходимых для подачи плагина в репозиторий сообщества. Теперь безопасность памяти Rust и скорость DuckDB идут рука об руку.
Открытый колоночный формат Lance, оптимизированный под ML и векторный поиск, теперь доступен и в DuckDB! Hao Ding реализовал поддержку чтения и записи таблиц Lance.
Писать данные можно так:
COPY (...) TO 'path/dataset.lance' (FORMAT lance, MODE 'overwrite');Для поиска доступны функции: `lance_vector_search()`, `lance_fts()` и `lance_hybrid_search()`.
Появилась библиотека `dux` — lazy-by-default (ленивые по умолчанию) датафреймы для Elixir поверх DuckDB. Конвейеры данных аккумулируются в AST структуре `%Dux{}` и компилируются в SQL CTE. Заявлено, что на тестах ($10$ млн строк, Apple M4 Max) Dux обгоняет Polars (Explorer) до 2.5 раз на операциях фильтрации.
Инструмент для трассировки ядра Linux `systing` (написанный Josef Bacik) перешел с сохранения логов Perfetto на прямую запись в DuckDB. А интеграция с Claude Code MCP (Model Context Protocol) позволяет ИИ динамически анализировать эти базы данных DuckDB в реальном времени.
Создано полноценное Go-ядро DuckDB для Jupyter, которое напрямую отправляет поток данных (Arrow IPC) во встроенный WASM-просмотрщик `hugr-perspective-viewer`. На панели также агрегируются метрики без написания SQL: `approx_unique`, `avg`, `min`, `max`, `count`.
Способен ли базовый ноутбук переваривать серьезную аналитику? Gábor проверил работу DuckDB на новом MacBook Neo с процессором Apple A18 Pro.
| Бенчмарк | Параметры | Результат (медиана) |
| ClickBench | 100M строк, лимит RAM: 5GB | < 1 секунды (cold run) |
| TPC-DS | SF100 | 1.63 секунды на запрос |
| TPC-DS | SF300 | 79 минут (высокий disk spill) |
Даже при 5 гигабайтах оперативной памяти DuckDB демонстрирует субсекундные ответы, эффективно утилизируя NVMe-память, когда RAM исчерпан (disk spill).
Стоит отдельно отметить профессора Dr. Torsten Grust из Тюбингенского университета (Германия). Его исследовательская группа, стоящая на стыке баз данных и технологий языков программирования, недавно запустила открытый курс DiDi (*Design and Implementation of DuckDB Internals*).
Курс использует DuckDB для обучения студентов архитектуре СУБД: от управления памятью и векторизованного исполнения до оптимизации запросов (включает около 50 рабочих примеров кода).
Экосистема DuckDB перестала быть просто *“SQLite для аналитики”*. С релизом DuckLake, нативной интеграцией протокола Postgres и появлением SDK для Rust, “утка” окончательно закрепилась как основополагающий инструмент в стеке современных данных.
Сегодня Gemini 3.1 Pro Preview расскажет свое мненИИе))
Связывание транзакционных баз (PostgreSQL) и аналитических хранилищ (ClickHouse) через прямые агрегации и `JOIN` часто приводит к жесточайшим блокировкам и деградации продакшена. Когда бизнес требует быстрый результат, а внедрение полноценного CDC (Debezium + Kafka) откладывается из-за сроков и сложности, лучшим решением становится пакетная и микро-пакетная выгрузка данных в озеро (в форматы Parquet и Apache Iceberg).
С точки зрения архитектуры, наша главная цель — минимизировать время загрузки данных T load и усилия инженеров на развертывание E setup. Наша целевая функция: min(T load × E setup)
В этой статье собраны исключительно рабочие, протестированные подходы для быстрой интеграции с озером данных (Data Lake) и аналитическим движком Trino.
Мы полностью исключаем создание и восстановление тяжелых дампов (`pg_dump`). Вся транзитная нагрузка ложится исключительно на асинхронные реплики.
Для задачи “результат нужен вчера и без сложного стека” идеально подходит OLake. Это высокопроизводительный движок репликации баз данных напрямую в Apache Iceberg (или Parquet), минуя промежуточные шины сообщений.
Шаг 1. Запуск сервиса (конфигурация `docker-compose.yml`):
version: '3.8'
services:
olake:
image: olakeio/olake:latest
ports:
- "8080:8080"
environment:
# Настройки доступов к вашему S3/MinIO
- AWS_ACCESS_KEY_ID=your_access_key
- AWS_SECRET_ACCESS_KEY=your_secret_key
- AWS_REGION=us-east-1Шаг 2. Запуск репликации:
Вы отправляете JSON-манифест в OLake (через UI или REST API). Движок самостоятельно делает первоначальный слепок PostgreSQL (Full Load со скоростью до 580K RPS), а затем переключается на чтение инкрементов (CDC):
{
"pipeline_name": "pg_to_iceberg_fast",
"source": {
"type": "postgres",
"connection_url": "postgresql://readonly_user:password@replica_host:5432/prod_db",
"tables": ["public.customer", "public.orders"]
},
"destination": {
"type": "iceberg",
"catalog_type": "rest",
"catalog_uri": "http://iceberg-rest:8181",
"warehouse_path": "s3://my-datalake/warehouse/"
},
"replication_mode": "full_and_cdc"
}Если вы хотите управлять выгрузкой через свои `cron`-задачи или Airflow, идеальным инструментом выступает аналитическая in-memory СУБД DuckDB. Ниже приведен протестированный Python-скрипт, который напрямую подключается к реплике и потоково перегоняет данные в Parquet на S3.
Рабочий скрипт на Python (`export_to_lake.py`):
import duckdb
# Открываем in-memory соединение DuckDB
con = duckdb.connect()
# 1. Устанавливаем и загружаем необходимые расширения
con.execute("INSTALL postgres;")
con.execute("INSTALL httpfs;")
con.execute("LOAD postgres;")
con.execute("LOAD httpfs;")
# 2. Настраиваем подключение к объектному хранилищу
con.execute("""
SET s3_region='us-east-1';
SET s3_access_key_id='YOUR_KEY';
SET s3_secret_access_key='YOUR_SECRET';
SET s3_endpoint='s3.your-domain.com';
""")
# 3. Подключаемся к реплике PostgreSQL
# Команда ATTACH монтирует Postgres прямо в DuckDB под именем 'pg'
con.execute("""
ATTACH 'host=replica_host port=5432 dbname=postgres user=postgres password=password'
AS pg (TYPE postgres);
""")
# 4. Копируем таблицу public.customer в S3 в сжатом формате Parquet
con.execute("""
COPY pg.public.customer
TO 's3://my-datalake/raw/customer.parquet'
(FORMAT PARQUET, COMPRESSION ZSTD);
""")
print("Выгрузка в Data Lake успешно завершена!")Данные из ClickHouse также необходимо перегружать в Озеро (для Trino), чтобы избежать дублирования логики таблиц и нагрузки на саму СУБД тяжелыми сторонними `JOIN`-ами.
Самый простой и не требующий дополнительной инфраструктуры способ — использовать встроенную функцию `s3()`. Она позволяет в один SQL-запрос отправить результат выборки прямо в объектное хранилище в нужном формате.
Пример выгрузки из ClickHouse в Parquet (выполняется в `clickhouse-client`):
-- Прямая вставка данных из локальной MergeTree таблицы в файл Parquet на S3
INSERT INTO FUNCTION s3(
'https://s3.us-east-1.amazonaws.com/my-datalake/raw/clickhouse_export/events_{_partition_id}.parquet',
'YOUR_KEY',
'YOUR_SECRET',
'Parquet'
)
SELECT id, event_type, payload, event_date
FROM local_events_mergetree
WHERE event_date = today();*Совет: Используйте макрос `{_partition_id}` в пути файла для автоматического разбиения больших выгрузок.*
Для построения архитектуры на десятилетие вперед разработчики из Altinity создали сборку Project Antalya. Она позволяет использовать таблицы Iceberg в S3 как *полноценное разделяемое хранилище*, работающее со скоростью локального диска, но обходящееся в 10 раз дешевле.
Пример прозрачного монтирования:
-- 1. Подключаем готовую Iceberg-таблицу прямо как движок ClickHouse
CREATE TABLE iceberg_customer
ENGINE = Iceberg('s3://my-datalake/warehouse/customer', 'aws_key', 'aws_secret');
-- 2. Запрашиваем данные. Теперь Trino и ClickHouse читают одни и те же Parquet-файлы!
SELECT count(*) FROM iceberg_customer WHERE status = 'active';
1. Управление оперативной памятью (OOM) в DuckDB
При скриптовой выгрузке гигантских таблиц in-memory движок может исчерпать RAM сервера.
Решение: Обязательно ограничивайте ресурсы сразу после
duckdb.connect():
con.execute("PRAGMA memory_limit='16GB'")
con.execute("PRAGMA threads=4")
2. Консолидация сложных типов данных PostgreSQL
Если в вашей таблице есть
JSONB,
UUIDили пользовательские массивы, Parquet может упасть с ошибкой соответствия типов.
Решение: Вместо
COPY pg.tableнапишите явный SQL-запрос с приведением к строке (
::VARCHAR):
con.execute("""
COPY (
SELECT id, metadata::VARCHAR AS metadata
FROM pg.public.customer
)
TO 's3://my-datalake/raw/customer.parquet' (FORMAT PARQUET);
""")Внутри Trino эти строки легко парсятся функциями вроде
json_extract().
3. Защита асинхронных реплик PostgreSQL от разрывов
Длительный процесс
SELECT *(или
COPY) мешает мастеру применять WAL-логи на реплике (из-за очистки строк VACUUM-ом).
Решение: На аналитической реплике (в файле
postgresql.conf) обязательно пропишите:
max_standby_streaming_delay = -1
max_standby_archive_delay = -1
hot_standby_feedback = onЭто позволит реплике “ставить на паузу” конфликтующие обновления и не обрывать ваш транзит данных.
В современных data-архитектурах часто возникает задача переноса реляционных данных в озера данных (Data Lakes). Если ваш стек включает PostgreSQL, Trino и Iceberg (например, с REST-каталогом Lakekeeper), возникает архитектурный вопрос: как переносить данные и обращаться к ним максимально эффективно?
В этой статье мы разберем два мощных подхода: использование “нативного” для Trino проталкивания через `system.query()` и применение расширения `pg_lake` на стороне базы данных.
Обычно в Trino мы пишем простой федеративный запрос:
SELECT * FROM postgres_catalog.public.customer WHERE acctbal > 1000;В идеальном сценарии оптимизатор Trino считывает предикат (`acctbal > 1000`) и транслирует его в SQL-диалект PostgreSQL. Это называется Pushdown (проталкивание).
Но на практике аналитические запросы гораздо сложнее. Если запрос содержит специфичную бизнес-логику, нестандартные оконные функции, сложные JOIN-ы или функции обработки строк, которых нет в базовом словаре коннектора Trino, оптимизатор не сможет транслировать этот кусок SQL. В результате Trino принимает решение скачать всю таблицу в память своих воркеров и применить фильтрацию уже там.
Особую роль при JOIN-ах играет механизм динамической фильтрации (Dynamic Filtering). Когда вы джоините большую таблицу из Postgres с маленькой таблицей (например, справочником из Hive/Iceberg), Trino сначала читает справочник (Build side), извлекает ключи, формирует SQL-фильтр (например, `IN (1, 2, 3)`) и на лету отправляет его в Postgres (Probe side).
Два критичных параметра в конфигурации коннектора управляют этим процессом:
В чем кроется опасность?
Если вычисление справочника на стороне Trino занимает больше времени, чем задано в `dynamic-filtering.wait-timeout` (например, 25 секунд против 20), координатор Trino прерывает ожидание. Чтобы не блокировать выполнение, он отправляет в Postgres “голый” запрос: `SELECT * FROM table`.
Вместо пары тысяч строк по сети внезапно начинают передаваться миллионы. Если загрузка сети — B, а объем таблицы PostgreSQL — V total, то время выполнения стремится к: T pull = B V total
что может привести к Out-of-Memory на воркерах Trino и падению кластера.
Чтобы гарантировать, что вычисления и фильтры 100% выполнятся на мощностях PostgreSQL, мы можем использовать специальную табличную функцию `system.query()`.
Этот подход разделяет обязанности: PostgreSQL занимается фильтрацией и тяжелой математикой локально, а Trino просто оркестрирует запись результата в Parquet/Iceberg.
-- Создаем таблицу в Iceberg (Lakekeeper) и наполняем её результатами из Postgres
CREATE TABLE iceberg_catalog.raw_data.customer_metrics WITH (
format = 'PARQUET',
partitioning = ARRAY['mktsegment']
) AS
SELECT
*
FROM
TABLE(
postgres_catalog.system.query(
query => '
-- Этот SQL выполняется СТРОГО внутри PostgreSQL
SELECT
custkey,
name,
mktsegment,
acctbal,
array_agg(acctbal) OVER (
PARTITION BY mktsegment
ORDER BY custkey
ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING
EXCLUDE GROUP
) AS rolling_bals
FROM public.customer
WHERE acctbal > 1000
AND created_at >= current_date - interval ''1 month''
'
)
);Преимущество: Если селективность нашего фильтра S равна 0.05 (остается 5% строк), то объем передаваемых по сети данных составит строго V total \ times S. Никакие таймауты Trino не заставят Postgres отдать лишние данные.
Если первый метод идеально подходит для использования Trino как движка трансформации, то зачем вообще существует проект `pg_lake`?
`pg_lake` внедряет под капот PostgreSQL движок DuckDB через `pgduck_server`. Это позволяет базе данных самостоятельно подключаться к S3 и читать/писать формат Iceberg, минуя Trino.
pg_lakeпрямо в PostgreSQL?
-- Объединение горячих данных из кучи (heap) PG и холодных данных из Iceberg
SELECT * FROM public.orders_current
UNION ALL
SELECT * FROM iceberg.orders_archive WHERE order_date < '2023-01-01';
https://github.com/lakekeeper/lakekeeper/tree/main/authz/opa-bridge
или тут https://docs.lakekeeper.io/docs/nightly/opa/
Много всего нового появилось у хранителя – роли, уточка и многое другое, статистика запросов
Немного сборной сборки про качество и ML
https://github.com/andkret/Cookbook
https://podcast.ru/e/3Ldlf9-6ebG
https://habr.com/ru/companies/vtb/news/762384/
Полезные ресурсы и ссылки:
Курс MLOps (OTUS): https://otus.ru/lessons/ml-bigdata/
Основные идеи из книги «Сотрудничество в DevOps-культуре»: http://agilemindset.ru/основные-идеи-из-книги-сотрудничест/
MLOps: Continuous delivery and automation pipelines in machine learning: https://cloud.google.com/architecture/mlops-continuous-delivery-and-automation-pipelines-in-machine-learning
Как создавать качественные ML-системы. Часть 1: каждый проект должен начинаться с плана: https://habr.com/ru/companies/vk/articles/749850/
Как создавать качественные ML-системы. Часть 2: приручаем хаос: https://habr.com/ru/companies/vk/articles/749852/
The Data Engineering Cookbook: https://github.com/andkret/Cookbook
Стандарты:
ISO/IEC DIS 5259-1: https://www.iso.org/standard/81088.html
SO/IEC DIS 5259-4: https://www.iso.org/standard/81093.html
ISO/IEC 8183:2023: https://www.iso.org/standard/83002.html
Работа с Big Data часто упирается в классическое “узкое горлышко”: кластер может обработать терабайты данных за секунды, но передача результатов (Result Set) обратно на сторону клиента (например, в Jupyter или скрипт) занимает часы. На дворе апрель 2026 года, и современные аналитические движки предлагают эффективные методы обхода этой проблемы — концепцию Spooling.
Немного душноты: https://www.starburst.io/blog/trino-spooling-protocol/
Архитектура Client Spooling в Trino создавалась с параноидальным акцентом на безопасность, в S3 выкидываются куски сырых, возможно, чувствительных данных.
Когда Trino решает сбросить данные в объектное хранилище, он всегда шифрует их на лету.
Для этого используется механизм S3 SSE-C (Server-Side Encryption with Customer-provided keys). Trino генерирует уникальный случайный AES-ключ для каждого запроса, отправляет его в MinIO вместе с данными, а клиенту (вашему Jupyter) отдает ссылку + этот же ключ для расшифровки.
Если мы используем локальный MinIO по адресу http://minio:9000 (без SSL/TLS), сервер MinIO видит, что ему пытаются передать секретный пароль (SSE-C ключ) по открытому незащищенному HTTP-каналу.
MinIO (как и настоящий AWS S3) строго запрещает это по спецификации. Он возвращает HTTP 400 Bad Request с ошибкой: “Requests specifying Server Side Encryption... must be made over a secure connection”. Поэтому тестировать лучше на реальном s3. И еще
Мгновенное удаление (Сборка мусора)
Главное правило Client Spooling: Trino удаляет файлы сразу же, как только они были прочитаны клиентом.
Как только ваш Python-скрипт или Jupyter получает ссылку на файл, скачивает его и отправляет координатору Trino HTTP-сигнал (ACK), что кусок получен, координатор дает команду немедленно удалить этот объект из S3.
Если запрос отменен или упал с ошибкой, Trino тоже моментально зачищает за собой fs.location. Вы просто не успеете их там увидеть.
Данных слишком мало (Thresholds)
Писать 10 строк в S3, генерировать для них Pre-signed URLs и отдавать клиенту — это дольше, чем просто плюнуть эти 10 строк текстом через координатор. Trino использует эвристику: если Result Set маленький, он отдается “инлайн” (внутри JSON-ответа самого координатора), и S3 не задействуется.
В этой статье мы разберем, как передавать результаты запросов через промежуточное S3-хранилище, на примере движков Trino и Apache DataFusion.
В классической архитектуре все воркеры кластера отправляют вычисленные строки на главный узел (Coordinator), а тот уже отдает их по одному каналу клиенту.
Если D — это объем результирующей выборки, а B c — пропускная способность сети координатора, то время выгрузки данных клиенту без спулинга равно:
T classic = B / Dc
В режиме Spooling координатор не гоняет данные через себя. Воркеры напрямую, параллельно пишут куски результата в дешевое объектное хранилище (S3/MinIO). Клиент получает лишь ссылки на эти файлы и скачивает их напрямую. Если у нас N файлов в S3, доступных для многопоточного скачивания с пропускной способностью клиента B client: T spooling ≈ min(N×B s3,B client)D
Это позволяет ускорить выгрузку в десятки раз, так как $B_{client}$ и распределенный $B_{s3}$ обычно значительно больше ограничений одного координатора.
Для демонстрации двух подходов мы убрали из нашего кластера все тяжелые клиентские среды (Jupyter, Spark) и оставили только “голое” ядро: хранилище S3, REST-каталог и SQL-движок.
docker-compose.yml
version: '3.8'
services:
minio:
image: minio/minio:latest
ports:
- "19000:9000"
- "19001:9001"
environment:
MINIO_ROOT_USER: "minio-root-user"
MINIO_ROOT_PASSWORD: "minio-root-password"
command: server /data --console-address ":9001"
minio-setup:
image: minio/mc:latest
depends_on:
- minio
entrypoint: >
/bin/sh -c "
sleep 5;
mc alias set myminio http://minio:9000 minio-root-user minio-root-password;
mc mb myminio/warehouse || true;
"
lakekeeper:
image: dalongrong/lakekeeper:latest
ports:
- "8181:8181"
environment:
- S3_ENDPOINT=http://minio:9000
- S3_REGION=us-east-1
- S3_ACCESS_KEY_ID=minio-root-user
- S3_SECRET_ACCESS_KEY=minio-root-password
depends_on:
- minio-setup
trino:
image: trinodb/trino:latest
ports:
- "8080:8080"
Сначала мы генерируем данные в Trino. Запрос
CREATE CATALOGиспользует динамическое подключение к Lakekeeper REST API. Скрипт записывает файлы в формате Parquet в MinIO:
config.properties
protocol.spooling.enabled=true
# 256-битный ключ в формате base64. Вы можете сгенерировать свой с помощью команды `openssl rand -base64 32`
protocol.spooling.shared-secret-key=jxTKysfCBuMZtFqUf8UJDQ1w9ez8rynEJsJqgJf66u0=
catalog.management=dynamicspooling-manager.properties
spooling-manager.name=filesystem
# Включаем чтение/запись в S3 для Spooling
fs.s3.enabled=true
# Путь внутри MinIO (указываем через s3://)
fs.location=s3://warehouse/client-spooling/
# Системные настройки S3 (MinIO)
s3.endpoint=http://minio:9000
s3.region=us-east-1
s3.aws-access-key=minio-root-user
s3.aws-secret-key=minio-root-password
s3.path-style-access=true-- 1. Подключение каталога Iceberg
CREATE CATALOG test_warehouse USING iceberg
WITH (
"iceberg.catalog.type" = 'rest',
"iceberg.rest-catalog.uri" = 'http://lakekeeper:8181/catalog/',
"iceberg.rest-catalog.warehouse" = '00000000-0000-0000-0000-000000000000/test_warehouse',
"iceberg.rest-catalog.security" = 'OAUTH2',
"iceberg.rest-catalog.nested-namespace-enabled" = 'true',
"iceberg.rest-catalog.vended-credentials-enabled" = 'true',
"fs.native-s3.enabled" = 'true',
"s3.region" = 'us-east-1',
"s3.path-style-access" = 'true',
"s3.endpoint" = 'http://minio:9000'
);-- 2. Создание структуры
CREATE SCHEMA test_warehouse.test_schema;
CREATE TABLE test_warehouse.test_schema.my_table (
id BIGINT,
data VARCHAR
) WITH (format = 'PARQUET');-- 3. Запись данных
INSERT INTO test_warehouse.test_schema.my_table VALUES (1, 'hello'), (2, 'world');Если написать Select – должно быть как-то так
Trino поддерживает протокол *Client Spooling* “из коробки” — когда Python-клиент запрашивает огромный `SELECT`, Trino сам незаметно пишет куски в S3 и отдает клиенту готовые ссылки.
В Apache DataFusion (который часто работает как локальный движок `datafusion-cli` или встраиваемая библиотка поверх S3) применяется более прозрачный паттерн делегирования (Explicit Spooling). Мы вручную инструктируем движок сохранить результаты агрегации в распределенное хранилище, чтобы позже забрать их в удобном формате — например, упаковав их в `JSON` и сжав алгоритмом `ZSTD`.
Запускаем `datafusion-cli`, передав доступы как переменные среды (для предотвращения ошибок парсинга опций):
AWS_ACCESS_KEY_ID="minio-root-user" \
AWS_SECRET_ACCESS_KEY="minio-root-password" \
AWS_ENDPOINT="http://localhost:19000" \
AWS_REGION="us-east-1" \
AWS_ALLOW_HTTP="true" \
datafusion-cliВнутри консоли подключаем директорию с Parquet-файлами, сгенерированными Trino:
CREATE EXTERNAL TABLE my_parquet_data
STORED AS PARQUET
LOCATION 's3://warehouse/019d81a3-c2d6-7ed2-ab15-070becf62582/my_table-13e4b91a2b4e47d98f312b1384263880/data/';Вместо того чтобы тянуть миллионы строк на локальный терминал, мы просим DataFusion выполнить преобразование и записать итог запроса обратно в MinIO.
Мы выбираем построчный JSON с экстремальным сжатием:
COPY (
-- Тут может быть любая сложная агрегация:
-- SELECT id, count(data) FROM my_parquet_data GROUP BY id
SELECT * FROM my_parquet_data
)
TO 's3://warehouse/019d81a3-c2d6-7ed2-ab15-070becf62582/my_table-13e4b91a2b4e47d98f312b1384263880/json_export/'
STORED AS JSON
OPTIONS (
'format.compression' 'zstd'
);Результат:
+-------+
| count |
+-------+
| 2 |
+-------+
1 row(s) fetched.
Elapsed 0.270 seconds.За миллисекунды (0.270 sec) DataFusion прочитал партиции, трансформировал бинарные столбцы в текст и сжал его.
Описанный паттерн выполнения команды `COPY TO` с сохранением `.json.zst` в MinIO полностью воспроизводит механику Spooling:
Еще немного про Fault-Tolerant Execution (FTE), нужно провести важную границу между архитектурой Trino (готовый распределенный кластер) и архитектурой DataFusion (ядро/библиотека выполнения запросов).
В самом “голом” ядре DataFusion (которое вы запускаете в `datafusion-cli` или в Jupyter) нет встроенного механизма Task Retries, потому что процессы выполняются на одной машине в рамках одного приложения. Если сервер падает — запрос прерывается.
Однако, в экосистеме DataFusion есть механизмы отказоустойчивости, которые делятся на два уровня: локальный (Spilling) и распределенный (Apache Ballista / Ray).
В Trino частой причиной падения задач является нехватка памяти (Out of Memory). В DataFusion реализован мощный механизм управления памятью.
Если DataFusion понимает, что оперативной памяти для агрегации или JOIN’а не хватает, он не “роняет” задачу, а начинает сбрасывать промежуточные данные на диск (Spill to Disk).
Trino использует архитектуру Fault-Tolerant Execution (FTE), при которой промежуточные результаты (Shuffle Exchange) пишутся в S3, а упавшие воркеры заменяются, и их задачи (Tasks) перезапускаются координатором.
В мире DataFusion эту задачу решает не само ядро, а распределенные планировщики, построенные поверх него:
Ballista — это надстройка над DataFusion, превращающая его в полноценный кластер (с Coordinator и Executors), архитектурно очень похожая на Apache Spark и Trino.
Сейчас огромную популярность набирает запуск DataFusion поверх кластера Ray.
Ray — это супер-устойчивый распределенный фреймворк. Интеграция `datafusion-ray` позволяет разбить SQL-запрос на граф задач прямо в Ray.
UPD: В локальном тестировании есть некоторые особенности. Когда контейнеры внутри имеют свою сеть, то трино посылает в dbeaver ссылки. А есть хост не знает что это за минива или localstack-spooling, то оно отдаст кусок данных, а остальные части просто не доедут. Квери упадет как отмененная, так как клиент получил не все результаты. Короче, надо просто так сделать
sudo nano /etc/hostsи вставить строку вашего s3 хоста.
127.0.0.1 localstack-spoolingто есть при спулинге клиент должен не только иметь сетевую связанность с s3 но различать dns имена корректно.
Короче сравния строк пройдено, все сошлося :)
со спулингом 2.2 сек
без спулинга 4.4 сек
Питончик 2.16 сек с чанками
в самом трино еще быстрее
все строки на месте: 150тыщъ
код !!
from trino.dbapi import connect
import json
TRINO_HOST = “localhost”
TRINO_PORT = 9999
TRINO_USER = “trino”
TRINO_CATALOG = “test_warehouse”
TRINO_SCHEMA = “test_schema”
OUTPUT_FILE = “output.json”
CHUNK_SIZE = 10000 # Количество строк, обрабатываемых за один раз
def export_to_json():
conn = connect(
host=TRINO_HOST,
port=TRINO_PORT,
user=TRINO_USER,
catalog=TRINO_CATALOG,
schema=TRINO_SCHEMA,
)
cursor = conn.cursor()
try:
cursor.execute(“SET SESSION retry_policy = ‘NONE’”)
cursor.execute(“SELECT * FROM my_table2”)
column_names = [desc[0] for desc in cursor.description]
row_count = 0
with open(OUTPUT_FILE, “w”, encoding=“utf-8”) as f:
while True:
rows = cursor.fetchmany(CHUNK_SIZE)
if not rows:
break
for row in rows:
row_dict = dict(zip(column_names, row))
f.write(json.dumps(row_dict, ensure_ascii=False, default=str) + “\n”)
row_count += len(rows)
print(f“Processed {row_count} rows...”)
print(f“Successfully exported {row_count} rows to {OUTPUT_FILE}”)
finally:
cursor.close()
conn.close()
if __name__ == “__main__”:
export_to_json()
Вот еще с уточкой и чанками
код
import duckdb
import json
OUTPUT_FILE = “/home/jovyan/examples/output_duckdb.json”
CHUNK_SIZE = 10000
conn = duckdb.connect()
conn.execute(“INSTALL httpfs; LOAD httpfs;”)
conn.execute(“INSTALL iceberg; LOAD iceberg;”)
conn.execute(“SET memory_limit = ‘4GB’;”)
conn.execute(“SET s3_region = ‘us-east-1’;”)
conn.execute(“‘’
CREATE OR REPLACE SECRET minio_secret (
TYPE S3,
KEY_ID ‘minio-root-user’,
SECRET ‘minio-root-password’,
ENDPOINT ‘minio:9000’,
USE_SSL false,
URL_STYLE ‘path’
);
‘‘’)
conn.execute(‘‘’
CREATE OR REPLACE SECRET iceberg_secret (
TYPE ICEBERG,
TOKEN ‘dummy’
);
‘‘’)
conn.execute(‘‘’
ATTACH ‘test_warehouse’ AS lakekeeper_db (
TYPE ICEBERG,
ENDPOINT ’http://lakekeeper:8181/catalog/',
ACCESS_DELEGATION_MODE ‘none’,
SECRET iceberg_secret
);
‘‘’)
cursor = conn.cursor()
cursor.execute(‘SELECT * FROM lakekeeper_db.test_schema.my_table2’)
col_names = [desc[0] for desc in cursor.description]
total_rows = 0
with open(OUTPUT_FILE, ‘w’, encoding=’utf-8’) as f:
while True:
rows = cursor.fetchmany(CHUNK_SIZE)
if not rows:
break
for row in rows:
row_dict = dict(zip(col_names, row))
f.write(json.dumps(row_dict, ensure_ascii=False, default=str) + ‘\n’)
total_rows += len(rows)
print(f’Обработано строк: {total_rows}’)
print(f’✅ Загружено и сохранено строк: {total_rows}”)
print(f“📁 Данные сохранены в {OUTPUT_FILE}”)
conn.close()
Можно даже так внутри уточки
import duckdb
OUTPUT_FILE = “/home/jovyan/examples/output_duckdb_direct.json”
conn = duckdb.connect()
conn.execute(“INSTALL httpfs; LOAD httpfs;”)
conn.execute(“INSTALL iceberg; LOAD iceberg;”)
conn.execute(“SET memory_limit = ‘4GB’;”)
conn.execute(“SET s3_region = ‘us-east-1’;”)
conn.execute(“‘’
CREATE OR REPLACE SECRET minio_secret (
TYPE S3,
KEY_ID ‘minio-root-user’,
SECRET ‘minio-root-password’,
ENDPOINT ‘minio:9000’,
USE_SSL false,
URL_STYLE ‘path’
);
‘‘’)
conn.execute(‘‘’
CREATE OR REPLACE SECRET iceberg_secret (
TYPE ICEBERG,
TOKEN ‘dummy’
);
‘‘’)
conn.execute(‘‘’
ATTACH ‘test_warehouse’ AS lakekeeper_db (
TYPE ICEBERG,
ENDPOINT ’http://lakekeeper:8181/catalog/',
ACCESS_DELEGATION_MODE ‘none’,
SECRET iceberg_secret
);
‘‘’)
conn.execute(f’’’
COPY (
SELECT * FROM lakekeeper_db.test_schema.my_table2
) TO ‘{OUTPUT_FILE}’ (FORMAT JSON);
‘‘’)
print(f’✅ Данные сохранены в {OUTPUT_FILE}’)
conn.close()
К конце концов я использовал
localstack-spooling
protocol.spooling.enabled=true
# 256-битный ключ в формате base64. Вы можете сгенерировать свой с помощью команды `openssl rand -base64 32`
protocol.spooling.shared-secret-key=jxTKysfCBuMZtFqUf8UJDQ1w9ez8rynEJsJqgJf66u0=
catalog.management=dynamicтак
spooling-manager.name=filesystem
fs.s3.enabled=true
fs.location=s3://spooling-bucket/client-spooling/
s3.endpoint=http://localstack-spooling:4566
s3.region=us-east-1
s3.aws-access-key=test
s3.aws-secret-key=test
s3.path-style-access=trueи так
services:
trino:
build: ./trino
environment:
- CATALOG_MANAGEMENT=dynamic
- LANCE_ALLOW_HTTP=true
- AWS_ALLOW_HTTP=true
- AWS_ACCESS_KEY_ID=minio-root-user
- AWS_SECRET_ACCESS_KEY=minio-root-password
- AWS_REGION=us-east-1
- AWS_ENDPOINT_URL=http://minio:9000
- CATALOG_MANAGEMENT=dynamic
- JDK_JAVA_OPTIONS=--add-opens=java.base/java.nio=ALL-UNNAMED --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED
healthcheck:
test: ["CMD", "curl", "-I", "http://localhost:8080/v1/status"]
interval: 2s
timeout: 10s
retries: 2
start_period: 10s
ports:
- "9999:8080"
volumes:
- ./lance5.properties:/etc/trino/catalog/lance5.properties
- ./lance_rest.properties:/etc/trino/catalog/lance_rest.properties
- ./lance_ice.properties:/etc/trino/catalog/lance_ice.properties
# --- ДОБАВЬТЕ ЭТУ СТРОКУ ---
- ./spooling-manager.properties:/etc/trino/spooling-manager.properties
# (При необходимости пробросьте и config.properties, если он не копируется при build: ./trino)
- ./config.properties:/etc/trino/config.properties
- spooling-data:/tmp/spooling
networks:
- lakekeeper-network
depends_on:
localstack-setup: # <--- Trino ждет, пока AWS CLI не создаст бакет!
condition: service_completed_successfully
localstack-spooling:
image: localstack/localstack:3.4.0 # Жестко фиксируем бесплатную рабочую версию!
container_name: localstack-spooling
ports:
- "4566:4566"
environment:
- SERVICES=s3
- AWS_DEFAULT_REGION=us-east-1
networks:
- lakekeeper-network
localstack-setup:
image: amazon/aws-cli:latest
container_name: localstack-setup
depends_on:
- localstack-spooling
restart: "no"
environment:
- AWS_ACCESS_KEY_ID=test
- AWS_SECRET_ACCESS_KEY=test
- AWS_DEFAULT_REGION=us-east-1
entrypoint: >
/bin/sh -c "
echo 'Waiting for LocalStack to fully start...';
sleep 10;
aws --endpoint-url=http://localstack-spooling:4566 s3 mb s3://spooling-bucket;
echo 'LocalStack bucket created successfully!';
"
networks:
- lakekeeper-network
jupyter:
image: quay.io/jupyter/pyspark-notebook:2024-10-14
depends_on:
lakekeeper:
condition: service_healthy
# Исправлено: теперь зависим от рабочего setup сервиса
lakekeeper-setup:
condition: service_completed_successfully
trino:
condition: service_healthy
# Удалено: starrocks (сервис не описан в compose файле)
command: start-notebook.sh --NotebookApp.token=''
volumes:
- ./notebooks:/home/jovyan/examples/
- spooling-data:/tmp/spooling
networks:
- lakekeeper-network
ports:
- "8888:8888"
# Сервис initialwarehouse УДАЛЕН, так как он дублировал lakekeeper-setup
# и ссылался на несуществующие сервисы (bootstrap, createbuckets).
postgres-lakekeeper:
image: postgres:17
container_name: postgres-lakekeeper
environment:
POSTGRES_USER: lakekeeper
POSTGRES_PASSWORD: lakekeeper
POSTGRES_DB: lakekeeper
ports:
- "5435:5432"
volumes:
- lakekeeper-postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U lakekeeper -d lakekeeper"]
interval: 2s
timeout: 10s
retries: 5
networks:
- lakekeeper-network
minio:
image: minio/minio:latest
container_name: minio-lakekeeper
environment:
MINIO_ROOT_USER: minio-root-user
MINIO_ROOT_PASSWORD: minio-root-password
# MINIO_DOMAIN: minio
command: server /data --console-address ":9001"
ports:
- "19000:9000"
- "19001:9001"
volumes:
- lakekeeper-minio-data:/data
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 2s
timeout: 10s
retries: 5
networks:
- lakekeeper-network
minio-setup:
image: minio/mc:latest
container_name: minio-setup
depends_on:
minio:
condition: service_healthy
entrypoint: >
/bin/sh -c "
mc alias set myminio http://minio:9000 minio-root-user minio-root-password &&
mc mb myminio/warehouse --ignore-existing &&
echo 'MinIO bucket created'
"
networks:
- lakekeeper-network
lakekeeper-migrate:
image: quay.io/lakekeeper/catalog:latest-main
container_name: lakekeeper-migrate
depends_on:
postgres-lakekeeper:
condition: service_healthy
environment:
- LAKEKEEPER__PG_ENCRYPTION_KEY=test-encryption-key-not-secure
- LAKEKEEPER__PG_DATABASE_URL_READ=postgresql://lakekeeper:lakekeeper@postgres-lakekeeper:5432/lakekeeper
- LAKEKEEPER__PG_DATABASE_URL_WRITE=postgresql://lakekeeper:lakekeeper@postgres-lakekeeper:5432/lakekeeper
restart: "no"
command: ["migrate"]
networks:
- lakekeeper-network
lakekeeper:
image: quay.io/lakekeeper/catalog:latest-main
container_name: lakekeeper
depends_on:
lakekeeper-migrate:
condition: service_completed_successfully
minio-setup:
condition: service_completed_successfully
environment:
- LAKEKEEPER__PG_ENCRYPTION_KEY=test-encryption-key-not-secure
- LAKEKEEPER__PG_DATABASE_URL_READ=postgresql://lakekeeper:lakekeeper@postgres-lakekeeper:5432/lakekeeper
- LAKEKEEPER__PG_DATABASE_URL_WRITE=postgresql://lakekeeper:lakekeeper@postgres-lakekeeper:5432/lakekeeper
- LAKEKEEPER__AUTHZ_BACKEND=allowall
- RUST_LOG=info
command: ["serve"]
healthcheck:
test: ["CMD", "/home/nonroot/lakekeeper", "healthcheck"]
interval: 2s
timeout: 10s
retries: 5
start_period: 5s
ports:
- "8282:8181"
networks:
- lakekeeper-network
lakekeeper-bootstrap:
image: curlimages/curl
container_name: lakekeeper-bootstrap
depends_on:
lakekeeper:
condition: service_healthy
restart: "no"
command:
- -w
- "%{http_code}"
- "-X"
- "POST"
- "-v"
- "http://lakekeeper:8181/management/v1/bootstrap"
- "-H"
- "Content-Type: application/json"
- "--data"
- '{"accept-terms-of-use": true}'
- "-o"
- "/dev/null"
networks:
- lakekeeper-network
lakekeeper-setup:
image: curlimages/curl
container_name: lakekeeper-setup
depends_on:
lakekeeper-bootstrap:
condition: service_completed_successfully
restart: "no"
entrypoint: ["/bin/sh", "-c"]
command:
- |
echo "Creating test_warehouse..."
curl -sf -X POST "http://lakekeeper:8181/management/v1/warehouse" \
-H "Content-Type: application/json" \
-d '{
"warehouse-name": "test_warehouse",
"project-id": "00000000-0000-0000-0000-000000000000",
"storage-profile": {
"type": "s3",
"bucket": "warehouse",
"endpoint": "http://minio:9000",
"region": "us-east-1",
"path-style-access": true,
"flavor": "minio",
"sts-enabled": false
},
"storage-credential": {
"type": "s3",
"credential-type": "access-key",
"aws-access-key-id": "minio-root-user",
"aws-secret-access-key": "minio-root-password"
}
}' && echo "Warehouse created successfully" || echo "Failed to create warehouse"
networks:
- lakekeeper-network
volumes:
lakekeeper-postgres-data:
lakekeeper-minio-data:
spooling-data:
networks:
lakekeeper-network:
driver: bridgeСсылка на оригинальную публикацию есть тут The 49MB Web Page.
Опубликовано: 12 апреля 2026 г. | Оригинал: 12 марта 2026 г.
МненИИе 🤖
Если бы отвлечение внимания пользователей было олимпийским видом спорта, новостные издания забирали бы все золотые медали. Зайдя на сайт крупного новостного портала вроде New York Times, чтобы просто прочитать пару заголовков, вы столкнетесь с лавиной: 422 сетевых запроса и 49MB загруженных данных. После того как страница наконец-то «успокоится» спустя пару минут, отпадает любой вопрос о том, почему каждый уважающий себя IT-специалист устанавливает блокировщики рекламы на все устройства своих близких.
Чтобы осознать масштаб феномена «49-мегабайтной веб-страницы», давайте вернемся в прошлое. Размер этой страницы превышает объем операционной системы Windows 95 (которая помещалась на 28 дискетах!). В эпоху расцвета iPod стандартный MP3-трек высокого качества (битрейт 192 kbps) занимал около 4-5MB. Таким образом, одна современная статья весит как полноценный музыкальный альбом из 10–12 песен.
Время загрузки в 2006 году ≈ 1.5 Mbps 49 MB×8 бит ≈ 261 секунда
Спустя 20 лет аппаратное обеспечение шагнуло далеко вперед, но современные рекламные технологии (ad-tech) полностью нивелировали этот прогресс своей плохой архитектурой и бесконечным раздуванием кода.
Издатели не злодеи, они просто в отчаянии. Попав в «смертельную спираль» programmatic-рекламы, они жертвуют долгосрочной лояльностью читателей ради сиюминутных копеек с показов (CPM). Современная рекламная индустрия разделила создателя контента и рекламодателя.
Каждое враждебное UX-решение проистекает из одной формулы: чем дольше вы заперты на странице взаимодействия, тем выше доход. Ваше разочарование — это их продукт. Мы можем описать общую стоимость взаимодействия (Interaction Cost) как математическую сумму:
C total =∑ ( C mental + C physical)
Вместо комфортного чтения пользователи сталкиваются с системой, которая максимизирует $C_{total}$, чтобы выжать максимум метрик из человеческого когнитивного ресурса.
Если маркетинговая команда настаивает на автовоспроизведении видео, разработчики обязаны использовать `IntersectionObserver`. Это позволит уважать ресурсы пользователя (батарею и CPU) при прокрутке страницы:
// Пример базовой реализации для паузы видео вне зоны видимости
const videoElement = document.querySelector('video.ads-player');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
videoElement.play();
} else {
videoElement.pause(); // Уважаем выбор пользователя!
}
});
});
observer.observe(videoElement);Также шапки сайтов следует скрывать при событии `scrollDown` и показывать только при `scrollUp`, освобождая драгоценное вертикальное пространство на мобильных устройствах.
Оригинальная статья поднимает важную проблему UI/UX, однако дискуссию стоит разбавить долей конструктивной критики:
Современный новостной веб-дизайн оказался в заложниках у метрик. Системы, созданные для вовлечения, трансформировались в «цифровую враждебную архитектуру», доводящую пользователя до ментального истощения. Страницы, превышающие по объему старые операционные системы, использование «тёмных паттернов» (модальные окна, микроскопические крестики закрытия) и беспощадная нагрузка на процессор телефона убивают самое главное — доверие между читателем и изданием.
Создателям контента следует помнить: если пользователь тратит свой когнитивный бюджет на то, чтобы закрыть 4 баннера до прочтения первого слова, никакая «оптимизация конверсии» не заставит его оформить платную подписку. Лучший веб-дизайн — это тот, который уважает время и внимание читателя.
Ventoy — это бесплатная утилита с открытым исходным кодом, которая навсегда изменит ваш подход к созданию загрузочных USB-накопителей. Вместо того чтобы каждый раз форматировать флешку для записи нового образа Windows или Linux, Ventoy позволяет просто копировать файлы образов на накопитель, как на обычную флешку.
Традиционные инструменты (например, Rufus или UltraISO) извлекают содержимое ISO-образа и записывают его на флешку, форматируя её. Если вам нужна другая операционная система, весь процесс приходится повторять.
Преимущества Ventoy:
Процесс использования максимально прост и состоит из нескольких шагов:
Несмотря на всю свою гениальность, у Ventoy есть несколько нюансов, о которых стоит знать:
Ventoy — это инструмент категории “must-have” для системных администраторов, энтузиастов и всех, кому приходится периодически переустанавливать операционные системы или пользоваться загрузочными инструментами. Один раз подготовив такую флешку, вы забудете о рутине с форматированием навсегда.
StarRocks — это аналитическая MPP-база данных нового поколения.
Если коротко, она пытается решить трилемму аналитики: объединить скорость ClickHouse (за счет векторизации и C++), гибкость Trino (поддержка сложных JOIN-ов) и простоту использования MySQL (совместимый протокол).
Это короткое руководство проведет вас от понимания архитектуры до построения простого конвейера загрузки данных (ETL) в домашнем продакшене.
В отличие от PostgreSQL (монолит) или ClickHouse (где узлы часто одноранговые), StarRocks имеет четкое разделение ролей. Это критически важно для понимания масштабирования и эксплуатации.
Написан на Java.
Написан на C++ (использует SIMD-инструкции процессора).
В Docker All-in-One: Оба компонента упакованы в один контейнер для удобства, но слушают разные порты:
- `9030`: FE (SQL интерфейс, сюда идет DBeaver).
- `8030`: FE (HTTP API для загрузки Stream Load, сюда идет curl).
- `8040`: BE (HTTP API метрик и логов).
Мы поднимем стек StarRocks и MinIO (S3-совместимое хранилище), используя bridge-сеть для связности.
Файл `docker-compose.yml` (Полностью рабочий пример):
version: "3.9"
networks:
starrocks-stack-network:
driver: bridge
services:
starrocks:
image: starrocks/allin1-ubuntu:4.0-latest
container_name: starrocks
hostname: starrocks.local.com
platform: "linux/amd64"
restart: unless-stopped
ports:
- "9030:9030" # MySQL Protocol (SQL клиенты)
- "8030:8030" # FE HTTP (Stream Load)
- "8040:8040" # BE HTTP (Logs/Metrics)
environment:
- TZ=UTC
networks:
starrocks-stack-network:
volumes:
# Персистентность данных (чтобы данные не исчезли после рестарта)
- ${HOME}/dv/starrocks/be/storage:/data/deploy/starrocks/be/storage
- ${HOME}/dv/starrocks/be/log:/data/deploy/starrocks/be/log
- ${HOME}/dv/starrocks/fe/meta:/data/deploy/starrocks/fe/meta
- ${HOME}/dv/starrocks/fe/log:/data/deploy/starrocks/fe/log
minio:
image: quay.io/minio/minio
container_name: minio
platform: "linux/amd64"
hostname: minio.local.com
restart: unless-stopped
ports:
- "9000:9000" # S3 API
- "9001:9001" # Web UI
networks:
starrocks-stack-network:
environment:
MINIO_ROOT_USER: root
MINIO_ROOT_PASSWORD: rootroot
volumes:
- ${HOME}/dv/minio/data:/data
command: server /data --console-address ":9001"Запуск:
`docker-compose up -d`
В StarRocks нельзя просто “создать таблицу”. Нужно выбрать тип ключа (Key Model), который определит, как база будет хранить и обновлять данные.
Подключение (DBeaver): `localhost:9030`, User: `root`, Password: (пусто).
CREATE DATABASE IF NOT EXISTS demo_db;
USE demo_db;Это “флагманская” возможность StarRocks. Она поддерживает быстрые Upsert (вставка новых или обновление старых записей по ID) в реальном времени.
CREATE TABLE IF NOT EXISTS users (
user_id INT NOT NULL,
username VARCHAR(50),
email VARCHAR(100),
register_date DATE,
city VARCHAR(50)
)
PRIMARY KEY (user_id) -- Уникальный ключ
DISTRIBUTED BY HASH(user_id) -- Распределение данных
PROPERTIES (
"replication_num" = "1" -- Для локального теста ставим 1 реплику
);База автоматически агрегирует данные при вставке. Если вы вставите новую продажу с *существующими* датой и категорией, StarRocks не создаст новую строку, а прибавит суммы к уже существующей строке. Это экономит место и ускоряет `GROUP BY`.
CREATE TABLE IF NOT EXISTS daily_sales (
report_date DATE NOT NULL,
category VARCHAR(50) NOT NULL,
-- Метрики с функцией агрегации:
total_amount BIGINT SUM DEFAULT "0",
items_sold INT SUM DEFAULT "0"
)
AGGREGATE KEY (report_date, category)
DISTRIBUTED BY HASH(report_date) BUCKETS 3
PROPERTIES (
"replication_num" = "1"
);Для загрузки данных в продакшене мы используем Service Account (Техническую учетную запись). Это стандарт безопасности: мы не используем `root` и не используем токены в конфигах (так как они требуют перезагрузки кластера для смены).
Выполнять под `root`:
-- 1. Создаем пользователя-бота
CREATE USER IF NOT EXISTS 'etl_loader'@'%' IDENTIFIED BY 'SecretPass123!';
-- 2. Даем права ТОЛЬКО на вставку и чтение в базе demo_db
GRANT INSERT, SELECT ON demo_db.* TO 'etl_loader'@'%';
-- Права применяются мгновенно.Stream Load — это самый быстрый способ загрузки (до 100 МБ/сек на узел). Он поддерживает транзакционность (ACID).
Пример файла `users.json`:
{
"users": [
{"user_id": 101, "username": "alex", "email": "a@test.com", "city": "NY"},
{"user_id": 102, "username": "bob", "email": "b@test.com", "city": "LA"}
]
}Команда загрузки (Terminal):
curl --location-trusted \
-u etl_loader:SecretPass123! \
-H "Expect: 100-continue" \
-H "format: json" \
-H "strip_outer_array: true" \
-H "json_root: $.users" \
-H "jsonpaths: [\"$.user_id\", \"$.username\", \"$.email\", \"$.city\"]" \
-H "columns: user_id, username, email, city" \
-T "users.json" \
-XPUT http://localhost:8030/api/demo_db/users/_stream_loadОтвет
{
"TxnId": 9596,
"Label": "a9a37ab6-3678-4c08-95b7-2fd8b6ae973e",
"Db": "demo_db",
"Table": "users",
"Status": "Success",
"Message": "OK",
"NumberTotalRows": 2,
"NumberLoadedRows": 2,
"NumberFilteredRows": 0,
"NumberUnselectedRows": 0,
"LoadBytes": 177,
"LoadTimeMs": 153,
"BeginTxnTimeMs": 2,
"StreamLoadPlanTimeMs": 2,
"ReadDataTimeMs": 0,
"WriteDataTimeMs": 26,
"CommitAndPublishTimeMs": 121
}%Давайте “дольем” данные в таблицу продаж. Агрегация произойдет на лету.
Файл sales.json (простой список):
[
{"dt": "2023-11-01", "cat": "Electronics", "amt": 100, "qty": 1},
{"dt": "2023-11-01", "cat": "Electronics", "amt": 50, "qty": 1}
]
curl --location-trusted \
-u etl_loader:SecretPass123! \
-H "format: json" \
-H "Expect: 100-continue" \
-H "strip_outer_array: true" \
-H "jsonpaths: [\"$.dt\", \"$.cat\", \"$.amt\", \"$.qty\"]" \
-H "columns: report_date, category, total_amount, items_sold" \
-T "sales.json" \
-XPUT http://localhost:8030/api/demo_db/daily_sales/_stream_loadОтвет:
{
"TxnId": 9613,
"Label": "bce0721a-dc2d-4927-be93-e0979a57873d",
"Db": "demo_db",
"Table": "daily_sales",
"Status": "Success",
"Message": "OK",
"NumberTotalRows": 2,
"NumberLoadedRows": 2,
"NumberFilteredRows": 0,
"NumberUnselectedRows": 0,
"LoadBytes": 143,
"LoadTimeMs": 52,
"BeginTxnTimeMs": 3,
"StreamLoadPlanTimeMs": 2,
"ReadDataTimeMs": 0,
"WriteDataTimeMs": 24,
"CommitAndPublishTimeMs": 20
}%Разбор заголовков:
Одна из сильных сторон StarRocks — способность “притворяться” другими базами данных для облегчения миграции.
Если у вас есть дашборды, написанные на диалекте Trino (Presto), вам не нужно переписывать все SQL-запросы.
Пример трансляции функций:
-- Функция Trino, которой нет в StarRocks
SELECT doy(date '2022-03-06');
-- Ошибка: No matching function...
-- Проверяем, как StarRocks переведет этот запрос
TRANSLATE TRINO select doy(date '2022-03-06');
-- Результат: SELECT dayofyear('2022-03-06')
-- Включаем режим автоматической трансляции в сессии
SET sql_dialect = 'trino';
-- Теперь запрос выполняется корректно, но это не правда. а вот так SELECT dayofyear('2022-03-06') работает. Может бага или у меня версия не та.
SELECT doy(date '2022-03-06');
-- Возвращаем нативный режим
SET sql_dialect = 'starrocks';*(Примечание: Поддержка диалекта постоянно расширяется, но некоторые специфические функции могут требовать ручной замены).*
| Характеристика | StarRocks | ClickHouse | Trino (Presto) |
| Основной сценарий | OLAP-витрины с JOIN-ами и обновлениями данных | Сбор логов, событий, метрик (Append-only) | Федерация данных (запрос к S3 + Postgres + Kafka одновременно) |
| JOIN производительность | ⭐⭐⭐ (Excellent, CBO оптимизатор) | ⭐ (Слабо, требует денормализации) | ⭐⭐⭐ (Excellent) |
| Обновление (UPDATE) | ⭐⭐⭐ (Работает как в OLTP, Primary Key) | ⭐ (Тяжелые асинхронные ALTER) | ❌ (Обычно только полная перезапись партиций), iceberg не в счёт :) |
| Язык Engine | C++ (SIMD Vectorized) | C++ (SIMD Vectorized) | Java (JVM) |
| Место в стеке | Serving Layer (Быстрый доступ для BI) | Storage Layer (Хранение логов) | Query Engine (Ad-hoc запросы к Data Lake) |
Выбирайте StarRocks, если:
Юрген Хабермас (18 июня 1929 — 14 марта 2026) — немецкий философ и социолог, крупнейший представитель второго поколения Франкфуртской школы, чье творчество оказало глубокое влияние на политическую философию, теорию права, этику и социальную теорию второй половины XX — начала XXI века . Его часто называли «самым главным философом Германии», наследником традиций Иммануила Канта и Карла Маркса . Он ушел из жизни 14 марта 2026 года в возрасте 96 лет.
Философский проект Хабермаса огромен, но в его основе лежит стремление защитить проект эпохи Просвещения (модерна) и переосмыслить природу разума в категориях не субъекта, а интерсубъективности и коммуникации.
1. Коммуникативная рациональность и теория коммуникативного действия
Это краеугольный камень его учения. Хабермас противопоставил инструментальному действию (ориентированному на успех и эффективность, характерному для сферы труда и бюрократии) действие коммуникативное. Коммуникативное действие — это взаимодействие индивидов, направленное на достижение взаимопонимания и консенсуса . В отличие от классической философии, идущей от отношения «субъект-объект», Хабермас предложил парадигму «субъект-субъект», где язык и дискурс становятся основой для выработки общих норм и ценностей .
2. Этика дискурса
Развитая совместно с Карлом-Отто Апелем, эта концепция предлагает новый взгляд на мораль. Место кантовского категорического императива, монологического по своей природе, занимает принцип дискурса: значимыми могут считаться только те нормы, с которыми согласились бы все участники дискуссии в условиях свободного от принуждения обсуждения .
3. Публичная сфера (Öffentlichkeit)
В своей ранней работе «Структурная трансформация публичной сферы» (1962) Хабермас описал, как в Новое время возникло пространство (салоны, кофейни, пресса), где частные лица могли собираться и обсуждать вопросы, представляющие общий интерес, формируя общественное мнение, способное контролировать власть . В конце жизни он вернулся к этой теме, анализируя кризис публичной сферы в эпоху цифровых платформ и социальных сетей .
4. Проект модерна и полемика с постмодернистами
Хабермас, в отличие от Жана-Франсуа Лиотара и других постмодернистов, отказывался считать проект модерна завершенным. Он видел в модерне «незавершенный проект», основанный на вере в силу разума. По его мнению, проблемы современности связаны не с провалом разума как такового, а с его искажением — доминированием одной лишь инструментальной рациональности, которая «колонизирует» «жизненный мир» (мир повседневного общения, семьи, культуры) .
5. Делиберативная демократия
Хабермас развил теорию демократии, в которой центр тяжести смещается с процедуры голосования на процесс открытого обсуждения (делиберации). Легитимность политических решений проистекает не просто из воли большинства, а из качества предшествующей дискуссии, в которой участвуют свободные и равные граждане .
6. Постсекулярное общество
В поздний период творчества Хабермас активно исследовал роль религии в современном мире, вводя понятие «постсекулярного общества». Это общество, в котором религия не исчезает, а продолжает существовать наряду с наукой и требует к себе толерантного отношения, при этом религиозные высказывания должны быть «переведены» на общедоступный язык для участия в общемировоззренческом дискурсе.
Хабермасу удалось создать масштабный и влиятельный теоретический синтез, соединив традиции немецкого идеализма, марксизма, прагматизма и аналитической философии .
Его главная удача — построение мощной альтернативы как пессимизму ранней Франкфуртской школы (Адорно, Хоркхаймер), так и релятивизму постмодернистов. Он предложил позитивную программу: вместо тотальной критики разума — его «перезагрузку» на коммуникативных основах .
Ему удалось ввести в академический и политический оборот понятия, ставшие общеупотребительными («коммуникативное действие», «публичная сфера», «делиберативная демократия»). Его идеи стали теоретической основой для развития гражданского общества и дискуссий о будущем Европейского Союза .
Он выиграл ключевые интеллектуальные споры своего времени, последовательно защищая универсалистские ценности Просвещения, свободу и демократию.
Главный упрек в адрес Хабермаса — чрезмерная идеализация «ненарушенной коммуникации». Критики, и с годами он сам это признавал, указывают на то, что его концепция «идеальной речевой ситуации» (свободного от власти и принуждения дискурса) является труднодостижимым идеалом в реальном мире, пронизанном отношениями господства и неравенством доступа к дискурсу .
Сложность и абстрактность его языка («непроходимый» стиль) делают его работы трудными для чтения не только для широкой публики, но порой и для специалистов.
В конце жизни, как отмечают некоторые критики, его анализ кризиса публичной сферы в эпоху интернета оказался недостаточно глубоким. Обеспокоенный «интернет-партизанами» и платформами, подрывающими, по его мнению, рациональный консенсус, он, возможно, недооценил глубину политического и социального кризиса западных обществ, списав его на сбои в работе медиа, а не на системные проблемы .
Юрген Хабермас на протяжении более чем шести десятилетий оставался «совестью» немецкой и европейской интеллигенции, неизменно выступая с позиций разума и эмансипации. Он оставил после себя не просто собрание сочинений, а целую школу мысли и набор инструментов для анализа общества.
Самым большим его вкладом в мировую философию по праву считается теория коммуникативного действия. Эта работа не только изменила оптику социальной теории, сместив фокус с субъекта на диалог, но и подарила надежду на то, что общество может быть устроено не только на основе рынка (денег) и административной власти, но и на основе солидарности и аргументированного консенсуса. Эта идея остается его главным интеллектуальным завещанием человечеству.