Представьте: 2:30 ночи. Офис пуст. Кофе давно остыл, а вы безучастно смотрите на экран, который издевательски показывает: Error: Could not convert string 'N/A' to INTEGER on line 56,789. Всё, что вам было нужно — импортировать «простую» CSV-выгрузку из вышестоящей системы. Знакомо?
Мы все бывали в этом «чистилище CSV». В тот самый момент, когда задача на пять минут превращается в многочасовое испытание, потому что где-то в глубине этого на первый взгляд безобидного файла скрывается неконсистентный разделитель, «сбежавшая» кавычка или, мой любимый случай — столбцы, которые загадочно появляются и вновь исчезают.
Я провёл бесчисленное количество часов, разбираясь с проблемными CSV-файлами, но после того как открыл для себя несколько малоизвестных возможностей DuckDB, такие ночные сражения стали происходить гораздо реже. Несмотря на то, что автоматический CSV-ридер DuckDB уже впечатляюще «умен», знание нескольких продвинутых приёмов позволяет избежать написания собственных скриптов предобработки, когда данные оказываются грязными.
В этом руководстве я поделюсь приёмами DuckDB, которые не раз спасали меня от CSV-ада:
-
Как диагностировать, каким DuckDB «видит» ваш CSV-файл
-
Подробный разбор анализатора (сниффер) CSV и принципов его работы
-
Способы обработки проблемных строк без прерывания всего импорта
-
Стратегии работы с несогласованными схемами в разных файлах
-
Тонкая настройка параметров, когда автоопределению требуется помощь
-
Практическая устойчивость: как DuckDB показывает себя на бенчмарке «грязных» CSV
Давайте разберёмся и попробуем сделать ваш следующий импорт CSV менее проблемным.
Что происходит «под капотом»: sniff_csv
Перед тем как загружать данные с помощью возможностей автоопределения DuckDB, крайне полезно понять, с чем, по мнению самой DuckDB, вы имеете дело. Правильно ли определён разделитель? Обнаружен ли заголовок? Какие типы данных были выведены? Функция sniff_csv() — это ваш инструмент разведки.
Вместо того чтобы вслепую запускать read_csv и рисковать получить ошибку, сначала выполните sniff_csv. Она анализирует выборку данных из файла и возвращает информацию об определённом диалекте, типах, наличии заголовка и других параметрах.
Возьмем в качестве примера файл events.csv:
EventDate|Organizer|City|Venue2022-03-01|OpenTech|San Francisco, CA|Moscone Center, Hall A2022-03-02|OpenTech|San Francisco, CA|Moscone Center, Hall B2022-03-03|OpenTech|San Francisco, CA|Moscone Center, Hall C
Посмотрим, как DuckDB интерпретирует этот файл:
FROM sniff_csv('events.csv');
ИНФО: Используются полные HTTP-адреса.
Для краткости в примерах ниже мы будем указывать только имя файла. Вы можете скачать примерные файлы по ссылкам (например, как events.csv выше). Однако можно также указать полный HTTP-адрес, так как DuckDB поддерживает протокол HTTP для загрузки CSV-файлов.
FROM sniff_csv(‘http://duckdb-example-files.s3-website-us-east-1.amazonaws.com/2025-blog-post-taming-wild-csvs/events.csv’);
Также можно управлять тем, какую часть файла анализировать:
FROM sniff_csv('events.csv', sample_size=10000); -- выборка 10 тыс. строк
Или проанализировать весь файл (осторожно с очень большими файлами!):
FROM sniff_csv('events.csv', sample_size=-1);
Результат возвращает большое количество информации в виде таблицы из одной строки:
-
Delimiter,Quote,Escape,NewLineDelimiter— определённые структурные символы. -
SkipRows— количество строк, которые, по мнению системы, нужно пропустить в начале. -
HasHeader— булевый флаг, указывающий, обнаружен ли заголовок. -
Columns— список структур с определёнными именами и типами столбцов (например,{'name': 'VARCHAR', 'age': 'BIGINT'}). -
DateFormat,TimestampFormat— обнаруженные форматы даты и времени (если есть). -
Prompt— крайне полезное поле. Оно содержит готовую командуread_csvсо всеми явно заданными параметрами. Её можно скопировать, вставить и при необходимости изменить.
Предварительный запуск sniff_csv позволяет существенно сократить количество догадок при сбоях импорта. Если, например, неверно определён разделитель или система считает, что заголовок есть, хотя его нет, вы сразу понимаете, какие параметры нужно переопределить в вызове read_csv.
Как работает анализатор
На самом деле CSV-анализатор DuckDB проходит несколько этапов, чтобы определить наилучший способ чтения файла:
-
Определение диалекта: на момент написания статьи анализатор проверяет 24 различных сочетания параметров диалекта (разделители, кавычки, экранирование, переводы строк), чтобы найти вариант, при котором число столбцов в строках оказывается наиболее согласованным.
-
Определение типов: после выбора диалекта анализатор изучает первый фрагмент данных (по умолчанию 2048 строк), чтобы определить типы столбцов, пытаясь приводить значения от наиболее специфичных типов к наименее специфичным (SQLNULL → BOOLEAN → BIGINT → DOUBLE → TIME → DATE → TIMESTAMP → VARCHAR).
-
Определение заголовка: анализатор проверяет, можно ли привести первую корректную строку к определённым типам столбцов. Если нельзя, эта строка считается заголовком.
-
Замена типов: если вы явно задали типы столбцов, они переопределяют типы, найденные анализатором.
-
Уточнение типов: затем анализатор проверяет найденные типы на большем объёме данных, используя векторизованное приведение (векторизацию).
Вот пример кода, который показывает, как посмотреть, что именно анализатор «видит» в более сложном случае:
-- Посмотреть, что анализатор определяет при большем размере выборкиSELECT Delimiter, Quote, Escape, SkipRows, HasHeader, DateFormat, TimestampFormatFROM sniff_csv('events.csv', sample_size=50000);-- Посмотреть определённые типы столбцовSELECT unnest(Columns)FROM sniff_csv('events.csv');
Когда я работал с набором данных, где было более 20 столбцов со смешанными типами, приём с unnest(Columns) оказался особенно полезным: он позволял точно увидеть, какие столбцы к каким типам были отнесены, и избавлял от множества повторных проверок и лишней отладки.
Разбираемся с ошибками: ignore_errors, store_rejects и не только
Итак, sniff_csv показывает, что всё в порядке, но с самим файлом проблемы всё равно остаются — например, среди миллионов корректных строк встречаются несколько проблемных. По умолчанию DuckDB остановит импорт на первой же ошибке. Но часто вам нужно просто загрузить корректные данные, а с плохими строками разобраться отдельно.
Вариант 1: просто пропустить их (ignore_errors)
Самый простой способ — указать DuckDB пропускать строки, которые вызывают ошибки разбора или приведения типов, с помощью параметра ignore_errors = true.
Представим файл faulty_folks.csv:
Name,AgeAlice,30Bob,forty-twoCharlie,35
Если попытаться прочитать этот файл обычным способом с явно заданными типами, импорт завершится ошибкой на возрасте Bob:
-- Это вызовет ошибку!SELECT * FROM read_csv('faulty_folks.csv', header=true, columns={'Name':'VARCHAR', 'Age':'INTEGER'});
Но если нам нужны только Alice и Charlie:
SELECT * FROM read_csv('faulty_folks.csv', header = true, -- Указываем ожидаемые типы columns = {'Name': 'VARCHAR', 'Age': 'INTEGER'}, ignore_errors = true -- Ключевой момент! );
Пояснение:
-
Мы задаём ожидаемые столбцы, включая тип
INTEGERдляAge. -
ignore_errors = trueсообщает ридеру: если встретится строка, где значение в столбце ‘Age’ нельзя привести кINTEGER(например, «forty-two»), просто отбрось эту строку и продолжай чтение.
Вывод:
|
Name |
Age |
|---|---|
|
Alice |
30 |
|
Charlie |
35 |
Bob остаётся за бортом, но импорт успешно завершается для корректных строк. Такой подход позволяет пропускать строки с самыми разными проблемами: ошибками приведения типов, неверным количеством столбцов, неэкранированными кавычками и так далее.
Вариант 2: пропустить и сохранить информацию (store_rejects)
Игнорировать ошибки удобно, но обычно всё же нужно понимать, что именно пошло не так и какие строки были отброшены. Возможно, вам нужно исправить исходные данные или подготовить отчёт по проблемам. Здесь параметр store_rejects = true становится особенно полезным.
Если использовать store_rejects, DuckDB по-прежнему пропускает плохие строки, как и при ignore_errors, но дополнительно записывает подробную информацию о каждой отброшенной строке и возникшей ошибке в две временные таблицы: reject_scans и reject_errors.
-- Читаем файл и сохраняем информацию об отброшенных строкахSELECT * FROM read_csv( 'faulty_folks.csv', header = true, columns = {'Name': 'VARCHAR', 'Age': 'INTEGER'}, store_rejects = true -- Сохранять сведения об ошибках -- Необязательно: можно задать свои имена таблиц и лимит -- rejects_scan = 'my_scan_info', -- rejects_table = 'my_rejected_rows', -- rejects_limit = 100 -- Сохранять максимум 100 ошибок на файл);-- Теперь посмотрим, что было отброшеноFROM reject_errors;-- И сведения о самом сканированииFROM reject_scans;
Пояснение:
-
Вызов
read_csvвыполняется, пропускает строку Bob и возвращает Alice и Charlie, как и раньше. -
Ключевое отличие в том, что
store_rejects = trueзаполняет временные таблицы. -
FROM reject_errors;показывает сведения о строках, которые не удалось обработать:
-
scan_id,file_id— ссылка на конкретное сканирование и файл. -
line— исходный номер строки в CSV. -
column_idx,column_name— в каком столбце возникла проблема, если это применимо. -
error_type— категория ошибки, напримерCASTилиTOO_MANY_COLUMNS. -
csv_line— фактическое содержимое отброшенной строки. -
error_message— конкретное сообщение об ошибке, сгенерированное DuckDB.
4. FROM reject_scans; возвращает метаданные о самой операции read_csv: разделитель, правило обработки кавычек, использованную схему, путь к файлу и другие параметры.
Я считаю это невероятно полезным при отладке «грязных» данных. Вы получаете загруженные корректные данные и подробный отчёт по отброшенным строкам — и всё это прямо в DuckDB. Больше не нужно использовать поиск по тексту (grep) по огромным файлам в попытке найти ту самую проблемную строку.
Вариант 3: ослабление правил (strict_mode=false и null_padding=true)
Иногда задача сводится к тому, чтобы просто загрузить данные — даже если они немного «кривые». В таких случаях помогают более «снисходительные» параметры разбора CSV в DuckDB.
Параметр strict_mode = false говорит DuckDB ослабить требования к формату. Он будет пытаться читать строки, даже если в них есть типичные проблемы:
-
Неэкранированные кавычки в полях (например,
"15" Laptop"). -
Строки с большим количеством столбцов, чем ожидается (DuckDB просто отбросит лишние).
-
Смешанные форматы перевода строки (например, одновременно
\nи\r\n).
ПРЕДУПРЕЖДЕНИЕ: быстро, но не всегда точно
Устанавливая
strict_mode=false, вы полагаетесь на то, что DuckDB сделает «наилучшее предположение». Это отлично работает, когда важна скорость, но если точность данных критична, результат стоит перепроверить.
Ещё один часто используемый параметр — null_padding = true. Он обрабатывает строки с недостаточным количеством столбцов. Вместо ошибки DuckDB просто заполняет недостающие значения NULL.
Рассмотрим пример. Пусть у нас есть «грязный» CSV-файл inventory.csv:
ItemID,Description,Price101,"15" Laptop",999.99102,"Wireless Mouse"103,"Mechanical Keyboard",129.99,ExtraField
Этот файл содержит:
-
Неэкранированную кавычку в описании первой строки
-
Отсутствующую цену во второй строке
-
Лишний столбец в третьей строке
Попробуем прочитать его обычным способом:
FROM read_csv('inventory.csv');
DuckDB пропустит все строки, кроме последней.
А теперь с ослабленными настройками:
-- Разбор «грязного» CSV с корректной обработкой недостающих и лишних полейFROM read_csv('inventory.csv', strict_mode = false, -- Игнорируем огрехи форматирования null_padding = true -- Заполняем недостающие столбцы значениями NULL);
Результирующая таблица:
|
ItemID |
Description |
Price |
column3 |
|---|---|---|---|
|
101 |
15 Laptop |
999.99 |
NULL |
|
102 |
Wireless Mouse |
NULL |
NULL |
|
103 |
Mechanical Keyboard |
129.99 |
ExtraField |
Работа с меняющимися схемами: union_by_name
Ещё одна распространённая проблема: нужно загрузить несколько CSV-файлов (например, ежемесячные отчёты), но структура столбцов в них немного отличается. Допустим, в марте добавился новый столбец, а в июне изменился порядок. Попытка прочитать их вместе через простой вызов read_csv('monthly_report_*.csv') может завершиться ошибкой или привести к некорректному выравниванию данных.
Параметр union_by_name = true в DuckDB решает эту задачу элегантно. При чтении нескольких файлов (через шаблоны или списки) он выравнивает столбцы по именам заголовков, а не по их позиции. Если в каком-то файле отсутствует столбец, который есть в других, он будет заполнен значением NULL.
Представим файл report_jan.csv:
UserID,MetricA,MetricB1,10,1002,15,110
И файл report_feb.csv:
UserID,MetricB,MetricC,MetricA3,120,xyz,204,125,abc,25
Обратите внимание на разный порядок столбцов и появление нового столбца MetricC в феврале.
SELECT *FROM read_csv( ['report_jan.csv', 'report_feb.csv'], -- Список файлов (или шаблон) union_by_name = true -- Вот в чём «магия»);
Пояснение:
-
DuckDB считывает заголовки из всех файлов.
-
Формируется объединённая схема со всеми уникальными именами столбцов (
UserID,MetricA,MetricB,MetricC). -
Для каждого файла данные сопоставляются с этой схемой по именам столбцов.
-
Если в файле отсутствует какой-либо столбец (например,
MetricCвreport_jan.csv), он заполняется значениямиNULL.
Результат:
|
UserID |
MetricA |
MetricB |
MetricC |
|---|---|---|---|
|
1 |
10 |
100 |
NULL |
|
2 |
15 |
110 |
NULL |
|
3 |
20 |
120 |
xyz |
|
4 |
25 |
125 |
Abc |
Тонкая настройка автоопределения и переопределение параметров
Хотя автоматическое определение параметров работает отлично, иногда вы знаете лучше, или выборка, на которой основывается DuckDB, оказывается недостаточно репрезентативной. Вот несколько способов точнее настроить процесс:
Увеличение выборки: если типы определяются некорректно (например, столбец в основном содержит целые числа, но из-за нескольких вещественных значений позже определяется как BIGINT), попробуйте увеличить размер выборки:
SELECT * FROM read_csv('file.csv', sample_size = 50000); -- Или проанализировать весь файл (может быть медленно для больших файлов)SELECT * FROM read_csv('file.csv', sample_size = -1);
Явное задание типов: переопределите типы столбцов, если автоопределение ошибается или вам нужен другой тип:
-- Переопределение по имени столбцаSELECT * FROM read_csv('file.csv', types = {'UserID': 'VARCHAR', 'TransactionAmount': 'DOUBLE'});-- Или по позиции, если нет заголовковSELECT * FROM read_csv('file.csv', header = false, types = ['VARCHAR', 'DOUBLE', 'DATE']);
Принудительное указание заголовка/его отсутствие: если определение заголовка работает некорректно (часто бывает, когда все столбцы выглядят как строки):
-- Принудительно указать наличие заголовкаSELECT * FROM read_csv('file.csv', header = true);-- Или отсутствие заголовка с заданием своих имён столбцовSELECT * FROM read_csv('file.csv', header = false, names = ['colA', 'colB', 'colC']);
Форматы даты и времени: если даты не в формате ISO 8601 (YYYY-MM-DD) или время задано нестандартно:
SELECT * FROM read_csv('file.csv', dateformat = '%m/%d/%Y', timestampformat = '%Y-%m-%dT%H:%M:%S.%f');
Все значения как строки (String): если вы хотите сначала загрузить всё как VARCHAR, а с типами разобраться позже:
SELECT * FROM read_csv('file.csv', all_varchar = true);
Какие столбцы могут быть NULL? По умолчанию пустое поле интерпретируется как NULL. Если пустые строки должны считаться допустимыми значениями:
SELECT * FROM read_csv('file.csv', force_not_null = ['column_name1', 'column_name2']);
Очистка имён столбцов: если в заголовках есть пробелы или нестандартные символы:
SELECT * FROM read_csv('file.csv', normalize_names = true);
Это автоматически приведёт имена к корректному виду (например, заменит все неалфавитно-цифровые символы на _ и т.д.) при импорте.
Парсер CSV в DuckDB: лидер бенчмарка Pollock
Для тех, кто действительно интересуется устойчивостью работы с CSV, существует любопытный бенчмарк под названием Pollock, который оценивает, насколько хорошо разные системы справляются с нестандартными CSV-файлами. Его создатели проанализировали более 245 000 публичных CSV-наборов данных, выявили типичные нарушения стандарта RFC-4180 и на их основе сформировали тестовые файлы.
В недавних тестах DuckDB занял первое место: при настройке на обработку проблемных файлов он корректно прочитал 99,61% данных во всех тестовых наборах. Даже в режиме автоопределения с минимальной настройкой DuckDB успешно обрабатывает около 90,75% данных.
Это практическое подтверждение того, что описанные в статье подходы позволяют справляться с подавляющим большинством реальных проблем, которые возникают при работе с CSV.
|
Тестируемая система |
Оценка Pollock(simple) |
Оценка Pollock(weighted) |
|---|---|---|
|
DuckDB 1.2 |
9.961 |
9.599 |
|
SQLite 3.39.0 |
9.955 |
9.375 |
|
UniVocity 2.9.1 |
9.939 |
7.936 |
|
LibreOffice Calc 7.3.6 |
9.925 |
7.833 |
|
SpreadDesktop |
9.929 |
9.597 |
|
SpreadWeb |
9.721 |
9.431 |
|
Python native csv 3.10.5 |
9.721 |
9.436 |
|
Pandas 1.4.3 |
9.895 |
9.431 |
|
MySQL 8.0.31 |
9.587 |
7.484 |
|
Mariadb 10.9.3 |
9.585 |
7.483 |
Двигаемся дальше базовых возможностей
Мы успели охватить довольно многое в этом разборе возможностей DuckDB для работы с CSV: от диагностики проблем с помощью sniff_csv до обработки ошибок через ignore_errors и store_rejects, объединения несовместимых схем через union_by_name и тонкой настройки всего процесса с помощью различных переопределений параметров.
Что мне особенно нравится в DuckDB — его CSV-ридер не сводится к простому инструменту загрузки. Это полноценный, продуманный инструмент, созданный для работы с реальной «грязью» в данных прямо внутри SQL. Большинство инструментов для работы с данными умеют читать идеальные CSV-файлы, но в повседневной работе действительно важно то, как они справляются с неидеальными.
Понимание этих чуть более продвинутых возможностей часто позволяет отказаться от внешних этапов предобработки и оставить всю логику загрузки данных внутри вашего SQL-процесса. В результате вы получаете более чистые пайплайны обработки данных, которые с меньшей вероятностью начнут сбоить, столкнувшись с неожиданными особенностями CSV.
В следующий раз, когда вам попадётся проблемный CSV, вспомните об этих приёмах. Они могут сэкономить вам немало времени и нервов и позволят быстрее вернуться к действительно интересной части работы — анализу данных. Удачных запросов!

Если CSV регулярно требует внешней предобработки, значит часть работы с данными вынесена за пределы базы. На курсе «SQL для разработчиков и аналитиков» разбирают, как обрабатывать и приводить данные прямо в SQL — от базового синтаксиса до оптимизации и работы с большими объёмами. Это упрощает пайплайны и снижает риск сбоев.
Готовы к обучению на SQL? Пройдите вступительный тест. До 30 апреля за прохождение теста действует скидка 15% на курс.
А чтобы узнать больше о формате обучения и задать вопросы экспертам, приходите на бесплатные уроки:
-
20 апреля 20:00. «Качество данных (data quality) на практике: от технических метрик до внедрения в команде». Записаться
-
21 апреля 20:00. «Функции в SQL: вычисления и переиспользование кода». Записаться
-
23 апреля 20:00. «Что нового в Spark 4.0». Записаться
ссылка на оригинал статьи https://habr.com/ru/articles/1024158/