Troubleshooting — DuckDB
База знаний типичных ошибок курса DuckDB.
Причина
Перенос ментальной модели PostgreSQL/MySQL на DuckDB. На самом деле DuckDB — in-process (embedded) движок: он исполняется внутри вашего процесса как библиотека.
Решение
- Не ищите сервер, демон, host/port, connection string по сети. Просто
pip install duckdbиduckdb.connect('my.db')— база живёт в файле или в памяти прямо внутри процесса. Это аналитический аналог SQLite. Сетевого протокола у DuckDB нет вовсе.
Причина
Распространённое заблуждение. DuckDB умеет out-of-core исполнение: при нехватке памяти он спиллит данные на диск.
Решение
- DuckDB поддерживает external hash aggregation, external hash join и external sort. Задайте
SET memory_limit='4GB'иSET temp_directory='/path/to/tmp'— тяжёлый GROUP BY/JOIN по датасету в десятки ГБ завершится на ноутбуке. Документированный кейс: все запросы на 50 ГБ датасете выполнены на 16 ГБ RAM. Spark для одной машины здесь не нужен.
Причина
DuckDB — single-process база: файл держит ровно один процесс. Внутри процесса возможны многопоточность и конкурентные читатели, но не несколько процессов-писателей.
Решение
- Архитектурно: один процесс-писатель на файл. Если нужна параллельная запись от многих процессов — это не профиль DuckDB. Варианты: писать в отдельные файлы и читать их вместе через glob; перейти на DuckLake с PostgreSQL-каталогом (настоящий multi-writer); или использовать MotherDuck. Для конкурентных читателей при одном писателе достаточно одного процесса.
Причина
DuckDB — OLAP-движок (колоночное хранение, векторизация). Частые мелкие INSERT/UPDATE отдельных строк от многих клиентов — это профиль OLTP.
Решение
- Не подменяйте PostgreSQL/SQLite DuckDB'ом для транзакционных приложений с высокой конкурентностью записи. DuckDB силён в аналитике: сканирования, агрегации, JOIN, отчёты. Для OLTP оставьте OLTP-СУБД; DuckDB подключайте как аналитический слой поверх (federated-запросы через postgres/sqlite-сканеры).
Причина
Расширение spatial не установлено и не загружено. В отличие от httpfs/parquet, spatial НЕ автозагружаемое.
Решение
- Выполните явно:
INSTALL spatial; LOAD spatial;— и только потом пространственные функции. Это надо делать в каждой новой сессии (LOAD), установка же разовая. Примечание: с DuckDB 1.5 тип GEOMETRY стал core-типом, но многие ST_-функции по-прежнему живут в расширении spatial.
Причина
Не все промежуточные структуры умеют спиллиться. Некоторые сложные агрегатные состояния (например, list-агрегаты, distinct внутри агрегатов, оконные функции с большими партициями) пока не выгружаются на диск.
Решение
- Снизьте
threads(каждый поток держит свой бюджет памяти), увеличьте memory_limit, переформулируйте запрос — разбейте тяжёлый GROUP BY на этапы, замените list-агрегаты, отфильтруйте данные раньше. Проверьте через EXPLAIN ANALYZE, какой оператор раздувается. Это известное ограничение, а не баг.
Причина
CSV-sniffer анализирует только выборку (по умолчанию 20 480 строк). Если аномальные значения (пустые строки, текст в числовой колонке) встречаются дальше выборки, типы выводятся неверно или поздно вскрывается ошибка.
Решение
- Увеличьте выборку:
read_csv('f.csv', sample_size=200000)илиsample_size=-1для анализа всего файла (медленнее, но надёжно). Лучше — задайте типы явно:read_csv('f.csv', columns={'id':'BIGINT','price':'DECIMAL(10,2)'}). Явная схема убирает и угадывание, и риск ошибки.
Причина
Параметр threads выставлен в 1, либо данные лежат в одном маленьком файле/одном row group и нечего параллелить, либо в плане доминирует pipeline breaker без параллельной реализации.
Решение
- Проверьте
SELECT current_setting('threads')— по умолчанию это число ядер. Запустите EXPLAIN ANALYZE и посмотрите, какой оператор занимает время. Для Parquet — несколько файлов или row groups дают параллельное сканирование. Один CSV без row groups параллелится хуже, чем Parquet.
Причина
По умолчанию DuckDB берёт значительную долю системной RAM (около 80%). В контейнере или на shared-машине это приводит к вытеснению других процессов или OOM-kill.
Решение
- Всегда задавайте лимит явно под своё окружение:
SET memory_limit='6GB'. В контейнере с cgroup-лимитом это критично — DuckDB может не видеть лимит контейнера. Заодно задайтеSET temp_directory='/tmp/duckdb', чтобы спилл шёл в известное место с достаточным объёмом диска.
Причина
Сжатие в DuckDB применяется только к персистентным базам. In-memory база (`:memory:`) хранит данные без колоночной компрессии.
Решение
- Если важен объём памяти — работайте с персистентным файлом (
duckdb.connect('data.db')), даже если он временный: данные лягут сжатыми сегментами. In-memory режим удобен для мелких сессий и тестов, но для больших датасетов берите файл. Грубая оценка: ~100 ГБ несжатого CSV -> ~25 ГБ файла DuckDB.
Причина
DuckDB гарантирует backward-совместимость (новая версия читает старые файлы), но НЕ forward-совместимость. Старый движок не обязан читать файл нового storage version.
Решение
- Обновите DuckDB до версии не ниже той, что создала файл. Если файл нужно отдать в окружение со старым DuckDB — пересоздайте его с нужной версией формата:
ATTACH 'old.db' (STORAGE_VERSION 'v1.2.0')и скопируйте данные, либо используйте флаг CLI-storage-version. Универсальный путь — EXPORT DATABASE в Parquet+SQL и IMPORT на другой стороне.
Причина
Расширение httpfs не загружено, либо нет доступа в интернет для автозагрузки, либо не настроены учётные данные (secret).
Решение
- httpfs автозагружаемо, но в air-gapped окружении установите заранее:
INSTALL httpfs. Для S3 задайте secret:CREATE SECRET (TYPE s3, KEY_ID '...', SECRET '...', REGION '...'). Без секрета приватные бакеты вернут 403. Проверьте также endpoint/region — для S3-совместимых хранилищ (MinIO, R2) их надо указывать явно.
Причина
Перенос привычки серверных СУБД, где данные сперва грузят в таблицы.
Решение
- DuckDB запрашивает Parquet/CSV/JSON/Arrow прямо на месте:
SELECT * FROM 'data/*.parquet' WHERE .... Pandas/Polars/PyArrow DataFrame тоже доступны по имени переменной через replacement scan — без CREATE TABLE и без копирования. Импорт в таблицу нужен, только когда вы хотите персистентность, сжатие или ускорение повторных запросов.
Причина
DuckDB часто сравнивают с Pandas из-за работы с DataFrame'ами в Python.
Решение
- DuckDB — полноценный SQL OLAP-движок: ACID-транзакции, настоящий cost-based оптимизатор, larger-than-memory исполнение, персистентное хранилище, параллелизм. Pandas — однопоточная in-memory библиотека без всего этого. DuckDB может запрашивать сам Pandas DataFrame, но возможностями он принципиально шире.
Причина
Файл .duckdb уже открыт другим процессом с эксклюзивной блокировкой: открытый DBeaver/IDE, другой Python-процесс, незавершённый предыдущий запуск, оставшийся .wal-lock после краха.
Решение
- Закройте все клиенты, держащие файл. Найдите процессы:
lsof file.duckdb(Mac/Linux). Для конкурентного ЧТЕНИЯ из второго процесса можно открыть базу в read-only:duckdb.connect('f.db', read_only=True)— read-only коннекты не конфликтуют. Писатель при этом должен быть один.
Причина
Переменная DataFrame недоступна в области видимости коннекта: запрос идёт через явно созданное соединение, DataFrame создан в другой функции/области, либо это не connection-метод, а другой объект.
Решение
- Replacement scan находит объект в локальной/глобальной области Python. Если переменная не видна — зарегистрируйте явно:
con.register('df', my_dataframe), затемcon.execute('SELECT * FROM df'). Явная регистрация надёжнее автоматического scan внутри функций и модулей.
Причина
Датасет не Hive-партиционирован, либо partition pruning/filter pushdown не сработал: фильтр по обычной колонке внутри файла, а не по партиционной колонке из пути.
Решение
- Организуйте данные в Hive-layout (
year=2026/month=05/) — тогда фильтр по партиционной колонке отсечёт целые каталоги. Для фильтра внутри файлов помогает Parquet с zonemap по row groups (filter pushdown пропустит группы). Проверьте через EXPLAIN ANALYZE число прочитанных байт с фильтром и без — так видно, работает ли pruning.
Причина
REAL/DOUBLE — типы конечной точности (IEEE 754). Это не потеря данных при сжатии (Chimp/Patas/ALP — lossless), а обычная природа float-арифметики.
Решение
- Для денег и точных значений используйте DECIMAL(p,s) — фиксированная точность без ошибок округления. DOUBLE оставьте для научных/приближённых расчётов. Сравнивайте float не на равенство, а с допуском. Сжатие чисел в DuckDB lossless — дело именно в самом типе float.
Причина
DuckDB — колоночный OLAP-движок: он оптимизирован под пакетную загрузку, а не под тысячи одиночных INSERT. Каждый отдельный INSERT — это транзакция и запись в WAL.
Решение
- Грузите данные пакетно:
INSERT INTO t SELECT * FROM read_csv('f.csv'),COPY t FROM 'f.parquet', либоCREATE TABLE t AS SELECT .... Из Python —con.execute('INSERT INTO t SELECT * FROM df')через replacement scan одним запросом. Множество одиночных INSERT — антипаттерн для OLAP.
Причина
Friendly SQL — это расширения именно диалекта DuckDB. FROM-first, EXCLUDE/REPLACE, COLUMNS, GROUP BY ALL, QUALIFY, list comprehensions не входят в стандарт SQL.
Решение
- Это ожидаемо: friendly SQL непереносим на PostgreSQL, Snowflake и др. Если запрос должен работать в нескольких СУБД — пишите стандартный SQL: явный список колонок, GROUP BY с перечислением, оконные функции в подзапросе вместо QUALIFY. Friendly SQL — для кода, привязанного к DuckDB.
Причина
Идентификаторы DuckDB регистронезависимы, но сохраняют исходный регистр. При этом строковые сравнения и поиск по имени могут вести себя не так, как ожидается, если колонка была создана в кавычках.
Решение
- Обращайтесь к колонке без кавычек —
OrderID,orderid,ORDERIDэквивалентны. Кавычки"OrderID"нужны, только если в имени есть пробелы/спецсимволы. По соглашению приводите имена к snake_case на этапе staging:"OrderID" AS order_id.
Причина
Коннект открыт с `read_only=True`, либо файл оказался read-only из-за прав ФС, либо процесс получил read-only fallback, потому что писатель уже держит файл.
Решение
- Откройте коннект на запись:
duckdb.connect('f.db')без read_only. Проверьте права файла и каталога. Убедитесь, что другой процесс не держит эксклюзивную блокировку (lsof). Read-only коннект нужен для конкурентного чтения рядом с писателем — но писать через него нельзя по определению.
Причина
Фильтр не протолкнулся к источнику: предикат стоит после JOIN/агрегации, или применён к вычисляемому выражению, по которому zonemap не работает, или у источника нет статистики.
Решение
- Фильтруйте как можно раньше — до JOIN. Не оборачивайте колонку фильтра в функцию (
WHERE date_col >= '2026-01-01', а неWHERE year(date_col) = 2026) — иначе zonemap и filter pushdown не сработают. Для таблиц убедитесь, что данные дают полезную min/max-статистику по сегментам.
Причина
CSV — текстовый строчный формат: его нужно каждый раз парсить, в нём нет колоночного хранения, сжатия, row groups и zonemap. Projection и filter pushdown по CSV ограничены.
Решение
- Если данные читаются многократно — один раз сконвертируйте:
COPY (SELECT * FROM read_csv('f.csv')) TO 'f.parquet' (FORMAT parquet). Дальше работайте с Parquet: колоночное чтение, сжатие, пропуск row groups по статистике, параллельное сканирование. CSV держите только как формат обмена.
Причина
Похожие названия. На деле это разные вещи: DuckDB — движок, DuckLake — формат lakehouse-таблиц.
Решение
- DuckLake — открытый формат lakehouse: данные в Parquet на object storage плюс ВСЕ метаданные в реляционной SQL-базе-каталоге (SQLite/PostgreSQL/DuckDB). DuckDB — движок исполнения и эталонная реализация DuckLake через расширение
ducklake. Можно использовать DuckDB без DuckLake и читать DuckLake другими движками (DataFusion, Spark, Trino).
Причина
Каждая запись по умолчанию могла бы порождать отдельный Parquet-файл — классическая проблема small files и write amplification в lakehouse-форматах.
Решение
- DuckLake решает это через data inlining: мелкие изменения (по умолчанию до 10 строк, параметр DATA_INLINING_ROW_LIMIT) хранятся прямо в метаданных каталога, а не в файлах. Когда данных накопилось — выполните
CHECKPOINT, чтобы выгрузить инлайненные данные в Parquet. Не делайте тысячи микро-коммитов без последующего checkpoint.
Причина
По умолчанию база DuckDB-WASM живёт в памяти вкладки. Без настройки персистентности состояние не сохраняется между перезагрузками.
Решение
- Используйте OPFS: открывайте базу по пути
opfs://my.db. Для автосохранения каждого изменения задайтеSET checkpoint_threshold='0KB'. Тогда состояние переживёт перезагрузку вкладки и даже перезапуск браузера. Учтите ограничения OPFS по объёму и то, что доступ к OPFS требует защищённого контекста (HTTPS).
Причина
Старая репутация ранних 0.x версий, когда формат менялся часто.
Решение
- Backward-совместимость формата гарантируется с версии v0.10 (storage version 64): новые версии DuckDB читают старые файлы. Forward-совместимость — best-effort, но управляема: параметр STORAGE_VERSION позволяет писать файлы, совместимые со старыми версиями. Для долговременной переносимости используйте EXPORT DATABASE.
Причина
Слишком много потоков при ограниченной памяти: каждый поток держит свой бюджет памяти, и общий расход растёт — начинается спилл или OOM. Либо запрос упирается в pipeline breaker, либо данных мало для параллелизма.
Решение
- Подбирайте threads под объём RAM: грубо ~1-2 ГБ/поток для агрегаций, ~3-4 ГБ/поток для тяжёлых JOIN. Если памяти мало — уменьшите threads, а не увеличивайте. Смотрите EXPLAIN ANALYZE: если время в одном breaker-операторе, рост числа потоков не поможет.
Причина
DuckDB 1.0 вышел в июне 2024 — он давно устарел. VARIANT, GEOMETRY как core-тип, MERGE INTO, encryption, DuckLake и friendly CLI появились позже.
Решение
- Ориентируйтесь на актуальные версии: stable — 1.5.2 (апрель 2026), LTS — линия 1.4.x «Andium» (поддержка до 16.09.2026). Для продакшена со строгими требованиями к стабильности берите LTS 1.4.x. MERGE INTO и encryption требуют минимум 1.4; VARIANT и GEOMETRY-core — минимум 1.5.
Причина
MERGE INTO появился только в DuckDB 1.4. На более старой версии этой команды нет.
Решение
- Обновитесь минимум до 1.4 (лучше — текущий stable 1.5.2 или LTS 1.4.x). До 1.4 для upsert используйте
INSERT ... ON CONFLICT DO UPDATE— но это требует ограничения уникальности/первичного ключа. Преимущество MERGE INTO как раз в том, что ключ не обязателен и условие слияния произвольное.
Причина
DuckDB оптимизирован под аналитику и пакетные изменения. Большие UPDATE/DELETE по колоночному хранилищу затрагивают целые row groups; накопление изменений в WAL без checkpoint тоже замедляет.
Решение
- Группируйте изменения в крупные операции вместо множества мелких. После больших серий записи выполняйте
CHECKPOINT, чтобы слить WAL в основной файл. Для массовой замены данных часто быстрееCREATE OR REPLACE TABLE t AS SELECT ..., чем точечные UPDATE/DELETE по строкам.