The Import/Export Wizard in SSMS is the right tool for one-off, small-file loads. For tens of millions of rows, you need to bypass the wizard entirely and talk to the SQL Server bulk-copy API directly. This article covers the tools and architectural settings that make that possible.
If you're new to SQL Server's import options and need the foundational overview first, see the complete SQL Server import guide before diving into the performance techniques here. PostgreSQL users looking for the parallel article — the "Drop-Load-Rebuild" pattern with COPY FREEZE — should see the Postgres bulk loading guide.
Method comparison
| Factor | BULK INSERT | bcp utility |
|---|---|---|
| Where it runs | Inside the SQL Server process | External CLI process |
| Scriptable in SQL | Yes — stored procedures, jobs | No — bash / PowerShell only |
| Parallel loading | Limited | Better — external process model |
| Error file support | Yes (ERRORFILE) | Yes (-e flag) |
| File must be local to server | Yes | No — runs from client machine |
| Speed ceiling | Excellent | Slightly faster for huge files |
Why Standard Inserts Fail at Scale
A standard INSERT INTO statement is fully logged for every row — SQL Server writes each change to the transaction log to ensure ACID compliance. Importing 10 million rows this way generates massive log growth and significant disk I/O overhead. The fix is minimal logging, which bulk-copy operations enable by streaming data directly into data pages instead of through the log.
1. BULK INSERT with TABLOCK — The T-SQL Path
BULK INSERT stays within the database engine but bypasses row-by-row locking overhead. The TABLOCK hint is what actually unlocks minimal logging — without it you're still locking at the row level and writing to the log.
-- Clear the staging table first for maximum load speed
TRUNCATE TABLE SalesData_Staging;
BULK INSERT SalesData_Staging
FROM 'D:\BigData\LargeDump2023.csv'
WITH (
FORMAT = 'CSV',
FIRSTROW = 2, -- Skip header row
FIELDTERMINATOR = ',',
ROWTERMINATOR = '0x0a', -- Hex for Linux-style line endings
TABLOCK, -- Enables minimal logging
BATCHSIZE = 100000, -- Commits in chunks — prevents log overflow
ROWS_PER_BATCH = 1000000
);Why TABLOCK is mandatory, not optional
In Simple or Bulk-Logged recovery models, TABLOCK tells SQL Server to skip row-level locking and use a table-level lock instead. This is the condition that enables minimal logging — which can cut import time by 50% or more on large files. Without it, you get full logging regardless of the recovery model.
2. The bcp Utility — The External Speed King
The Bulk Copy Program communicates directly with the SQL Server bulk-copy API as an external process. Because it runs outside the SQL Server process, it can be faster than BULK INSERT on very large files and is easily scripted in batch files or PowerShell.
bcp MyDatabase.dbo.SalesData_Staging in "C:\Data\HugeFile.csv" ^ -c ^ # Character mode — correct for text CSV -t, ^ # Comma field terminator -S MyServer ^ # Server name -T ^ # Trusted connection (Windows Auth) -b 50000 ^ # Batch size — commit every 50k rows -h "TABLOCK" # Pass table lock hint for minimal logging
-b 50000— commits every 50,000 rows, preventing the transaction log from growing unbounded on a massive file.-h "TABLOCK"— passes the locking hint through to the server; same effect as the T-SQLTABLOCKoption.- Replace
-Twith-U username -P passwordfor SQL authentication.
3. Database Architecture Tweaks
Even the best script hits a ceiling if the database configuration works against it. Three settings account for most of the remaining performance gap.
A. Switch to Simple Recovery Model
In Full Recovery Model, every bulk operation is fully logged — even with TABLOCK. Temporarily switching to Simple or Bulk-Logged removes that constraint for the duration of the import.
ALTER DATABASE MyDatabase SET RECOVERY SIMPLE; -- Perform bulk load here -- ALTER DATABASE MyDatabase SET RECOVERY FULL;
Take a full backup before switching recovery models
Switching to Simple Recovery breaks the log backup chain. If the server crashes during the load, you cannot restore to a point in time before the crash. Always take a full database backup immediately before switching, and restore to Full Recovery as soon as the load completes.
B. Drop Non-Clustered Indexes Before Loading
Every inserted row forces SQL Server to update every non-clustered index's B-Tree — in real time, for millions of rows. Dropping those indexes before the load and rebuilding them after is almost always faster than maintaining them during the load. This is the same "Drop-Load-Rebuild" principle that applies in PostgreSQL bulk loading.
- Script out your non-clustered indexes (
DROP INDEXstatements). - Perform the
BULK INSERTorbcpload. - Recreate the indexes (
CREATE INDEX) — SQL Server builds them from scratch in a single sorted pass, which is much faster than updating them row-by-row.
C. Always Load into a Staging Table First
Never bulk-load directly into your production tables. A staging table has no constraints, no foreign keys, and no triggers — which eliminates the per-row overhead that slows production inserts. Once the data is validated in staging, move it to production with a MERGE or INSERT ... SELECT.
Staging table design
Use VARCHAR(MAX) or NVARCHAR(MAX) for all columns — nothing can fail on a type error during load. Then clean, cast, and validate in SQL before inserting into the production schema. The same pattern in PostgreSQL is covered in the schema evolution guide.
4. Handling Dirty Data at Scale: Error Files
When importing millions of rows, even a 0.01% error rate means thousands of bad rows. By default, a single bad row aborts the entire batch. Both BULK INSERT and bcp support an error file that skips bad rows and records them for review — without halting the import.
BULK INSERT SalesData_Staging
FROM 'C:\Data\HugeFile.csv'
WITH (
FORMAT = 'CSV',
TABLOCK,
ERRORFILE = 'C:\Logs\ImportErrors.log', -- Bad rows written here
MAXERRORS = 100 -- Abort only if more than 100 rows fail
);After the load, review ImportErrors.log to understand the failure pattern — encoding issues, type mismatches, and truncation errors each produce distinct signatures. The CSV import troubleshooting guide covers the most common patterns and their fixes, even though it uses PostgreSQL examples — the underlying data quality issues are identical across both platforms.
Summary Checklist for Maximum Speed
Using TABLOCK? Without it, you're locking row-by-row and logging every write.
Database in Simple or Bulk-Logged recovery? Full recovery logs everything even with TABLOCK.
Non-clustered indexes dropped? Active indexes force real-time B-Tree updates per row.
Loading into a staging table? Direct production loads carry constraint and trigger overhead.
File local to the SQL Server? Network paths introduce latency on every read — move the CSV to a local drive first.
BATCHSIZE set? Without it a failed load mid-way rolls back the entire import.
By combining bcp or BULK INSERT with the right locking hints, recovery model, and staging architecture, SQL Server can ingest data at rates exceeding 1 GB per minute — turning an overnight job into a coffee-break operation.
Read next
SQL Server CSV Import: All 5 Methods
The foundation — wizard, BULK INSERT, bcp, OPENROWSET, and SSIS
Bulk Loading Millions of Rows into PostgreSQL
The Postgres parallel — COPY FREEZE, Drop-Load-Rebuild, maintenance_work_mem
Troubleshooting CSV Import Errors
Type mismatches, encoding issues, and permission denials — fixed
Staging Table Strategy for CSV Imports
Why TEXT staging beats direct production loading every time
Stop tuning bulk loads manually
Elvity's automated onboarding engine handles schema inference, validation, and high-throughput ingestion — no TABLOCK hints, recovery model switches, or index rebuild scripts required.