Импорт CSV живёт в CLI, а не в SQL
В диалекте SQL у SQLite нет инструкции IMPORT. Загрузка CSV — это возможность командной оболочки sqlite3, точечная команда .import. Тут важно перестроить мышление, если вы пришли из MySQL с его LOAD DATA INFILE или из Postgres с COPY: там команды выполняются на стороне сервера, а .import — это уже работа клиентской утилиты, которая сама читает файл и под капотом выполняет INSERT.
Поэтому всё, о чём пойдёт речь дальше, подразумевает, что вы находитесь внутри оболочки sqlite3:
sqlite3 mydata.db
Если же вам нужно загружать данные из кода приложения — на Python, Node или Go — то CSV вы будете читать средствами самого языка и складывать в базу через параметризованные запросы INSERT. Этот подход мы разберём в главе про интеграцию с приложениями. А сейчас речь именно про CLI.
Базовый импорт через .import
Самый короткий путь — сказать SQLite, что файл в формате CSV, и натравить .import на сам файл, указав имя таблицы.
.mode csv
.import people.csv people
В зависимости от того, существует ли уже таблица people, произойдёт одно из двух:
- Таблицы нет — SQLite создаст её сам, взяв имена столбцов из первой строки CSV. Все столбцы получат тип
TEXT. - Таблица уже есть — SQLite вставит в неё все строки файла как данные. Строка с заголовками, если она есть, попадёт туда же как обычная запись.
Именно на втором сценарии чаще всего и спотыкаются при первой попытке импорта. Если у вашего CSV есть заголовок, а таблица уже создана — заголовок нужно пропустить явно.
Как пропустить заголовок при загрузке CSV в существующую таблицу SQLite
Передайте .import флаг --skip 1, чтобы он проигнорировал первые N строк:
CREATE TABLE people (
name TEXT,
age INTEGER,
city TEXT
);
.import --csv --skip 1 people.csv people
--csv — это сокращение для .mode csv, действующее только в рамках этой команды, так что отдельно режим выставлять не нужно. --skip 1 отбрасывает строку с заголовками. Все оставшиеся строки попадают в таблицу people в порядке столбцов.
Быстрая проверка после импорта:
SELECT count(*) FROM people;
SELECT * FROM people LIMIT 5;
Порядок столбцов в файле должен совпадать с порядком столбцов в таблице. Никакого сопоставления по заголовкам тут нет — .import просто кладёт N-е поле в N-й столбец, и всё.
Пусть SQLite сам создаст таблицу
Если вы только щупаете данные, проще всего вообще обойтись без CREATE TABLE — пусть .import сам соберёт таблицу по заголовку CSV-файла:
.mode csv
.import sales.csv sales
.schema sales
.schema sales покажет примерно такое:
CREATE TABLE sales(
"order_id" TEXT,
"amount" TEXT,
"ordered_at" TEXT
);
Обрати внимание: все колонки получились TEXT. Это сделано намеренно — .import не пытается угадывать типы. Если хочешь, чтобы amount был настоящим числом, а ordered_at — нормальным таймстампом, сначала создай таблицу руками с нужными типами, а потом импортируй с флагом --skip 1. Дальше за дело возьмётся type affinity SQLite: числовые строки сами приведутся к INTEGER и REAL при вставке.
Свои разделители: TSV, pipe, точка с запятой
Режим .mode csv работает с запятой. Для файлов с табуляцией переключаем режим:
.mode tabs
.import data.tsv events
Если разделитель отличается от стандартного, задайте его через .separator после выбора режима:
.mode csv
.separator "|"
.import pipe_data.txt events
Стоит запомнить один момент: .mode csv работает по правилам экранирования из RFC 4180 — поля с запятыми или переводами строк внутри корректно обрабатываются, если они заключены в кавычки ". А вот .mode tabs — это простой режим разбиения по символу, без какого-либо экранирования. Если в вашем файле есть поля в кавычках с разделителями внутри, оставайтесь в .mode csv и просто поменяйте разделитель.
Разбираем на реальном примере
Допустим, файл orders.csv выглядит так:
order_id,customer,amount,ordered_at
1001,Ada,49.99,2026-01-12
1002,Boris,12.50,2026-01-13
1003,"Chen, Wei",199.00,2026-01-14
Обратите внимание: в третьей строке внутри поля в кавычках стоит запятая. Вот полный лог сессии:
В реальной оболочке весь блок INSERT заменяется одной командой .import --csv --skip 1 orders.csv orders. Поле "Chen, Wei" остаётся целым, потому что режим CSV корректно обрабатывает кавычки. Колонка amount приходит как настоящее число, а order_id — как целое, и всё благодаря типам столбцов.
Импорт CSV внутри транзакции
.import выполняет по одному INSERT на каждую строку. Для пары тысяч записей это нормально. А вот на миллионе строк такой подход становится мучительно медленным — если только не обернуть всю загрузку в транзакцию, чтобы SQLite не фиксировал изменения после каждой строки:
BEGIN;
.import --csv --skip 1 big_file.csv events
COMMIT;
Одна эта мелочь превращает импорт длиной в несколько минут в дело пары секунд. Если что-то пойдёт не так посреди загрузки, ROLLBACK откатит частично загруженные данные — это же удобно и при повторных попытках.
Ускориться можно ещё сильнее: удалите индексы до импорта и пересоздайте их после. Перестроение индекса на каждой строке съедает кучу времени.
Типичные ошибки и как их лечить
Error: expected N columns but found M — количество полей в строке не совпадает с числом колонок таблицы. Обычные причины:
- Лишняя запятая внутри незакавыченного поля. Перевыгрузите файл с корректным экранированием по CSV или переключитесь с
.mode tabsна.mode csv(по стандарту RFC 4180). - Пустая строка в конце файла. Поправьте файл вручную или поиграйтесь с
--skip. - В таблице больше колонок, чем в CSV. Либо добавьте недостающие поля в файл, либо залейте данные в промежуточную (staging) таблицу нужной формы, а оттуда уже скопируйте в боевую.
Строка с заголовками попала в данные — забыли указать --skip 1 при загрузке CSV в существующую таблицу SQLite. Удалите эту строку (DELETE FROM t WHERE rowid = 1) и запустите импорт заново уже с флагом.
Числа сохранились как строки — вы дали .import создать таблицу самому, и все колонки получились TEXT. Удалите таблицу, опишите её руками с типами INTEGER/REAL и импортируйте CSV заново.
Error: no such file — путь считается относительно того каталога, откуда вы запустили sqlite3, а не относительно файла базы. Используйте абсолютный путь либо сделайте cd в нужную папку до запуска оболочки.
CLI выводит номер строки, на которой споткнулся, — это самый быстрый способ найти проблемную запись в большом файле.
Кратко повторим
.import— это dot-команда CLI, а не SQL. Запускать её нужно внутри оболочкиsqlite3.- Флаг
--csvкорректно обрабатывает кавычки, а--skip 1пропускает строку с заголовками. - Если таблицы нет,
.importсоздаст её по заголовку CSV — но все колонки будутTEXT. Лучше создать таблицу самому, чтобы получить нормальные типы. - Большие импорты заворачивайте в
BEGIN/COMMIT, иначе на каждую строку будет своя транзакция. - Порядок колонок в файле должен совпадать с порядком колонок в таблице.
Дальше: выгружаем данные обратно
Импорт — это только половина истории. Та же оболочка умеет выгружать результаты запросов или целые таблицы обратно в CSV, JSON или SQL. Пригодится для бэкапов, дата-пайплайнов и передачи данных в другие инструменты. Об этом поговорим в разделе про экспорт данных.
Часто задаваемые вопросы
Как загрузить CSV-файл в базу SQLite?
Откройте базу через консольную утилиту sqlite3, переключитесь в CSV-режим командой .mode csv и выполните .import data.csv table_name. Если таблицы ещё нет, SQLite создаст её сам, взяв имена колонок из первой строки файла. Если таблица уже существует — все строки файла попадут в неё как данные, поэтому почти всегда нужен флаг .import --skip 1, чтобы пропустить шапку.
Как импортировать CSV с заголовком в уже существующую таблицу SQLite?
Запустите .import --csv --skip 1 data.csv table_name. Флаг --skip 1 говорит SQLite пропустить первую строку, чтобы заголовок не превратился в обычную запись. Без него в таблице появится строка, где значениями будут литеральные названия колонок.
Почему импорт CSV в SQLite падает с ошибкой 'expected N columns but found M'?
В файле есть строки, у которых количество колонок не совпадает с таблицей — обычно из-за запятых внутри значений, неэкранированных кавычек или пустой строки в конце. Используйте .mode csv (или флаг --csv) вместо .mode tabs — тогда SQLite разберёт кавычки по RFC 4180. И загляните в файл текстовым редактором: CLI выводит номер проблемной строки, по нему битую запись находишь за секунды.