🗄️ File Operations · 10 min read
CSV to MySQL: Why Rows Go Missing on Import (2026)
Start with an explicit FIELDS clause. LOAD DATA defaults to tab-separated input, so a comma-separated file loaded without one puts the whole line into the first column. If the error mentions local data, the server variable local_infile is off by default in current releases and the client needs to opt in too. After that, most remaining problems are type widths, encodings and warnings that nobody read. The four routes are below, followed by the failure list that accounts for nearly every half-loaded table.
The complaints in r/SQL and r/dataanalysis tend to be about a partial load rather than a refusal. One thread on importing CSV into MySQL contains a line that sums up the experience: “MySQL Workbench not importing all rows from csv”, followed by the equally common “best way to import csv file into Mysql”, where the accepted answer is that mysqlimport cuts the data and the wizard behaves differently again. The file is rarely the problem — the statement usually is.
The default that catches almost everyone
LOAD DATA is an old statement with old defaults, and the manual says so in as many words: with no FIELDS or LINES clause, the behaviour is the same as FIELDS TERMINATED BY '\t', ENCLOSED BY '', ESCAPED BY '\\' and LINES TERMINATED BY '\n'. Tab separated. So this fails:
-- Loads every line, intact, into the first column
LOAD DATA LOCAL INFILE '/tmp/customers.csv'
INTO TABLE customers;And this works, because the file now describes itself to the parser:
LOAD DATA LOCAL INFILE '/tmp/customers.csv'
INTO TABLE customers
CHARACTER SET utf8mb4
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\r\n'
IGNORE 1 LINES
(@id, @name, @signup)
SET id = NULLIF(@id, ''),
name = TRIM(@name),
signup = STR_TO_DATE(@signup, '%Y-%m-%d');Five of those clauses are doing real work. CHARACTER SET utf8mb4 stops the connection default from mangling accented names. OPTIONALLY ENCLOSED BY makes RFC 4180 quoting work, including a doubled quote inside a quoted field and a line break inside one. LINES TERMINATED BY '\r\n' matches a file exported on Windows, where a bare newline can leave a carriage return glued to the last column. IGNORE 1 LINES skips the header. The trailing SET block reads each field into a user variable first, so you can turn an empty string into a real NULL and parse the date explicitly instead of hoping MySQL guesses the format.
Why local loading is refused
LOAD DATA without LOCAL means the server opens the file, which requires the FILE privilege and a directory allowed by secure_file_priv. LOAD DATA LOCAL INFILE means your client reads the file and streams it, which needs neither, and is consequently the variant people reach for.
The manual states that local_infile is disabled by default and notes that this changed from earlier versions, so a server upgrade turns it off on its own. Both sides must agree: enabling the server variable while the client still refuses gives the same error. The client opt-in is what --local-infile=1 does when you start the mysql client, and MySQL Workbench 8.0 tightened the same behaviour, which is one reason the wizard started failing for people who had used it for years.
The reason for the caution is documented as well: with LOCAL, a server you do not control can request files from your machine, not just the one you named. On a shared or third-party database that is a real consideration rather than a theoretical one. On the database you run yourself, enabling it for the duration of the import and turning it off afterwards is a reasonable habit.
The four routes, and what each one is for
| Route | File lives | Needs | Realistic size | Notes |
|---|---|---|---|---|
LOAD DATA INFILE | On the server | FILE privilege, secure_file_priv | Very large | Fastest, and awkward if you do not have shell access |
LOAD DATA LOCAL INFILE | On your machine | Server and client both allow local loading | Large | The usual choice for a laptop to a remote server |
mysqlimport | On your machine | Same as local loading | Large | Command-line front end to LOAD DATA; the table name comes from the file name |
util.importTable in MySQL Shell | On your machine | MySQL Shell | Largest | Chunks the file and loads it over several threads |
| Workbench import wizard | On your machine | A GUI session | Thousands of rows | Convenient and clickable; the slowest of the five |
Two things are worth knowing about the parallel utility, since it is the least familiar option. It splits one file into chunks and loads them concurrently with LOAD DATA LOCAL INFILE underneath, so it is not a different engine, just several of the same one at once. It also means a file with a syntax error inside a quoted field can fail in a chunk rather than at the top, and the error message points at the chunk boundary rather than the bad line.
Create the table first, and read the header twice
LOAD DATA does not create tables for you, and this is where a lot of imports go wrong. A wizard that infers types from the first fifty rows will call a postcode a number and a money column a float. Declare the types deliberately:
CREATE TABLE customers (
id VARCHAR(20) NOT NULL PRIMARY KEY, -- text: keeps leading zeros
name VARCHAR(120) NOT NULL,
signup DATE NULL,
revenue DECIMAL(12,2) NULL -- never FLOAT for money
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;Identifiers are text even when they look numeric. A customer number of 0001234567 becomes 1234567 in an INT column, and an 18-digit reference number loses its last digits in a float. Money is DECIMAL, not FLOAT or DOUBLE, because binary floating point cannot represent 0.10 exactly and the rounding error accumulates over a column of thousands of rows. Declare the table with the column widths matching the longest value in the file, since a VARCHAR that is too short is a truncation warning today and a rejected row once strict mode is on.
If the CSV contains accented characters or anything outside the Latin-1 range, the table and the connection both need utf8mb4. That is four bytes per character, and it covers characters outside the basic multilingual plane — the older three-byte utf8 in MySQL could not store those. Picking the three-byte form today is how an import half-succeeds with question marks in the text and no error at all.
When rows go missing
| Symptom | Cause | Fix |
|---|---|---|
| Whole line in column one | Default separator is a tab | FIELDS TERMINATED BY ',' |
| Header text sitting in the first row | No IGNORE clause | IGNORE 1 LINES |
| Some rows absent, no error shown | Field count mismatch, or values a column refused | SHOW WARNINGS straight after the load |
| Last column full of stray characters | Windows line endings read as a bare newline | LINES TERMINATED BY '\r\n' |
| Rows split in the middle of a field | Comma or line break inside a quoted field | Add OPTIONALLY ENCLOSED BY |
| Accented names replaced by question marks | Connection or table charset is not utf8mb4 | Set both, and see garbled text fixes |
| Load stops on a duplicate key | PRIMARY KEY already holds the value | Use REPLACE or IGNORE, or load into a staging table |
| Import dies partway through a big batch | Statement exceeded max_allowed_packet | Load from a file rather than via INSERT batched in the client |
The third row of that table is the expensive one. A row with fewer fields than the table has columns does not stop a LOAD DATA statement; it produces a warning and the row is skipped. Because the statement itself returns successfully, a script that checks only for an exception reports success. Compare SELECT COUNT(*) against the number of lines in the file, minus the header, after every import. When the two disagree, the warnings hold the reason.
The duplicate key case deserves a word too, because it interacts with the route you chose. A file reloaded after a failed attempt usually contains rows that did arrive the first time, so the second run hits the primary key. Loading into a staging table with no constraints and then inserting with a join is slower to write and much easier to re-run, which matters more than the extra minute when a partner sends a corrected file at 5pm.
Making a big import quicker
Three changes cover most of it. Load into a table without secondary indexes and add them afterwards, because every index has to be updated for every row. Use LOAD DATA rather than INSERT statements, since a statement that carries thousands of rows beats thousands of statements, and the client-side packet limit caps how many rows fit in one anyway. And if you must insert row by row, wrap the batch in one transaction instead of committing each row, which removes a disk flush per row.
Once the data is in, the questions usually change from how to load it to what it says. If the CSV was a one-off that will not be reloaded, it is often quicker to ask the question of the file directly with the CSV analyzer than to build a schema for it. Related ground on this site: CSV to SQL covers turning the file into INSERT statements for a database you cannot reach from the command line, querying a CSV with SQL skips MySQL entirely by using DuckDB or SQLite, and changing the delimiter is the fix when the separator is something other than a comma in the first place. For SQLite rather than MySQL, the .import route has no privileges to configure at all.
Frequently asked questions
Why did my whole CSV file land in one column?
Because LOAD DATA has no idea your file is a CSV. The MySQL manual states that with no FIELDS or LINES clause the defaults behave as FIELDS TERMINATED BY a tab, ENCLOSED BY an empty string, ESCAPED BY a backslash and LINES TERMINATED BY a newline. A comma-separated line therefore holds no tab, so the entire line is one field and everything goes into column one. Add FIELDS TERMINATED BY ',' to the statement and the columns line up.
Why does LOAD DATA LOCAL INFILE say loading local data is disabled?
Because both ends have to agree and neither one is on by default any more. The MySQL manual is explicit that local_infile is disabled by default on the server, and that this changed from earlier releases, so an upgrade can switch local loading off without anyone editing a config file. The client has to opt in as well, for example by starting the mysql client with local-infile enabled. Setting the server variable alone leaves the client still refusing.
What is the difference between LOAD DATA INFILE and LOAD DATA LOCAL INFILE?
Without LOCAL, the server opens the file itself, so the path is a path on the database server, the account needs the FILE privilege and secure_file_priv has to permit the directory. With LOCAL, the client reads the file from your machine and streams it over the connection, so the path is a path on your laptop and no FILE privilege is involved. The LOCAL variant carries a documented risk: a server you do not control can ask the client for other files that the client can read, which is the reason local loading is off by default.
How do I stop the header row from becoming a data row?
Add IGNORE 1 LINES to the statement. Without it the header is loaded as the first record, and the damage is worse than a stray row: a name column gets a varchar value where the header text was, and a decimal column receives the word total, which under MySQL's default strict mode raises an error or a truncation warning depending on the value. If the file has a couple of banner lines before the header, put the count in the same clause.
Why were some rows imported and others skipped with no error?
Run SHOW WARNINGS immediately after the statement, because that is where the answer is. A row containing fewer fields than the table has columns produces a warning and is skipped rather than failing the whole load, and rows whose values a column cannot accept are either truncated or rejected depending on the SQL mode. MySQL 8.0 ships with a default SQL mode that includes STRICT_TRANS_TABLES, NO_ZERO_IN_DATE and NO_ZERO_DATE, so an invalid date such as 0000-00-00 or 31 February stops the row instead of being stored as a zero date. The count of skipped rows is in the warnings, not in an exception.
How do I handle commas and quotes inside the data?
Tell the statement about the CSV quoting rules and let the parser deal with it. Add OPTIONALLY ENCLOSED BY with a double quote character, and LOAD DATA recognises a field that opens with that character and treats a doubled quote inside it as a literal quote, which is exactly what RFC 4180 specifies. A quoted field may also contain a line break, and that no longer ends the record, so a single row can span several physical lines in the file. A script that splits the file on newlines before importing cannot do this, which is one good reason to let the database read the file instead of writing INSERT statements yourself.
What is the fastest way to import a very large CSV into MySQL?
Use LOAD DATA rather than INSERT statements, and use MySQL Shell's parallel table import utility, util.importTable, when the file is measured in gigabytes. It splits the file into chunks and loads them over several threads with LOAD DATA LOCAL INFILE underneath, which is the only mainstream route into MySQL that uses more than one connection. Two further savings apply to any route: create the table without its secondary indexes, load, then add the indexes afterwards, and avoid autocommit overhead on row-by-row inserts by sending them in batches inside one transaction.
Why does my import fail with Data truncated for column?
Usually a type or length mismatch rather than a corrupt file. A column declared as INT UNSIGNED receives a negative value, a DECIMAL(5,2) receives 1234.56, a VARCHAR(20) receives a 40-character string, or a DATETIME receives text in a format the parser does not accept. Under strict mode MySQL rejects the value, and outside strict mode it stores a rounded or clipped version and logs a warning, which is how tables end up with wrong numbers and a clean import log. Check the declared widths against the longest value in each column before loading rather than after.
Tools mentioned in this guide
The import is free with the MySQL client. These three help when the file arrives monthly, from a partner, with a new column every time:
- OpenCode Go — the loader script is twenty lines until someone sends a file with a different column order, and a $10-a-month subscription covering 19+ models is cheaper than one evening of print debugging a LOAD DATA statement. Try OpenCode Go
- Stack AI — if the CSV lands in cloud storage on a schedule, a workflow can pick it up, validate the column count and load it, so a bad file fails loudly instead of importing half a table. Try Stack AI
- Softr — when the point of the import is that colleagues can look rows up, publishing a searchable page over the same database beats teaching everyone a SELECT statement. Try Softr
Some links above are affiliate links — if you buy through them we may earn a commission at no extra cost to you. Links currently point to each vendor's official page until our dedicated tracking links are registered. OpenCode Go uses our referral link.
Not Ready for a Schema?
Upload the CSV and ask your question in plain language. Column types, indexes and import flags are somebody else's problem.
Related reading
File Operations — other guides that pair well with this one.
- Wireless Keyboard & Mouse for Excel Under $30
- Best Monitor for Spreadsheets: Pivot, Ultrawide or 27-inch?
- Excel Slow With Large Data
- Cheap USB Drives & Portable SSDs Under $50 for CSV
Browse all guides in the NoCodeCSV blog.