MongoDB → SQL Server Migration
Full-load and live Change Stream replication from MongoDB (on-prem, Atlas, or Cosmos Mongo API) to SQL Server using MigrationBridge. Covers schema inference, document-to-relational mapping, type conversions, and post-migration hardening.
Overview & Document-to-Relational Mapping
MongoDB is a document-oriented NoSQL store; SQL Server is a relational engine. MigrationBridge bridges the gap by flattening each collection into a SQL table and mapping BSON field types to SQL Server column types. The table below summarises the structural transformation rules applied during every migration run.
| MongoDB Concept | SQL Server Concept | Notes |
|---|---|---|
| Database | Database | Mapped 1:1 by name (configurable) |
| Collection | Table (dbo.<collection_name>) |
Collection name becomes table name; schema defaults to dbo |
| Document field | Column | Field name becomes column name (sanitized — see Step 2) |
_id field |
Excluded | Not migrated. A new INT IDENTITY(1,1) PRIMARY KEY column named _row_id is created instead |
| Nested object (sub-document) | NVARCHAR(MAX) JSON string |
Full document structure preserved as JSON; use JSON_VALUE / OPENJSON to query |
| Array field | NVARCHAR(MAX) JSON string |
Array serialised as a JSON array string; no exploding/normalisation |
| Schema (implicit) | DDL CREATE TABLE |
Inferred from first 1,000 documents per collection; widest compatible type wins |
_id field (ObjectId or custom) is never migrated to SQL Server. A surrogate integer identity column _row_id INT IDENTITY(1,1) PRIMARY KEY is created automatically. If you need to preserve the original _id value, add a VARCHAR(24) column manually and populate it via a separate export step before migrating.Prerequisites — Source (MongoDB)
Python Driver
MigrationBridge uses pymongo to connect to MongoDB. Ensure it is installed in the MigrationBridge Python environment:
pip install pymongo[srv]
The [srv] extra is required for Atlas mongodb+srv:// connection strings. For plain TCP connections it is optional but harmless.
Connection String Formats
| Target | Connection String | Notes |
|---|---|---|
| Local / On-Prem | mongodb://localhost:27017 |
Default port 27017; adjust host/port as needed |
| MongoDB Atlas | mongodb+srv://user:pass@cluster.mongodb.net/ |
Requires DNS SRV resolution; use M30+ tier for 30-day oplog retention |
| Cosmos DB Mongo API | mongodb://account.mongo.cosmos.azure.com:10255/?ssl=true&replicaSet=globaldb |
SSL required; replicaSet=globaldb must be present; some operators unsupported (see Known Issues) |
Required MongoDB Permissions
readAnyDatabase— read access across all databases (or at minimumreadon the target database)clusterMonitor— required to callrs.status()and tail the oplog for Change Streams
// Grant in mongosh (admin database)
db.grantRolesToUser("migrationbridge", [
{ "role": "readAnyDatabase", "db": "admin" },
{ "role": "clusterMonitor", "db": "admin" }
])
Replica Set Requirement
MongoDB Change Streams (live replication) require the source to be a replica set or sharded cluster. Standalone mongod instances do not support Change Streams. Full-load-only migrations work against standalone instances.
Pre-Flight Checks
// 1. Connectivity ping
db.adminCommand({ "ping": 1 })
// Expected: { ok: 1 }
// 2. Replica set status (required for Change Streams)
rs.status()
// Look for: "stateStr" : "PRIMARY" in the members array
// If standalone: rs.status() returns an error -- Change Streams unavailable
Prerequisites — Target (SQL Server)
SQL Server Permissions
The migration account needs the following privileges on the target database. Least-privilege option is listed first; db_owner is the broadest option and easiest to grant in dev/test.
| Permission | Purpose | Role / Grant |
|---|---|---|
CREATE TABLE |
Schema inference creates tables | GRANT CREATE TABLE TO [migrationbridge] |
INSERT |
Load rows into target tables | GRANT INSERT ON SCHEMA::dbo TO [migrationbridge] |
TRUNCATE |
Re-run without PK conflicts | Requires ALTER TABLE permission or db_owner |
db_owner |
Broadest — includes all of the above | ALTER ROLE db_owner ADD MEMBER [migrationbridge] |
ODBC Driver
MigrationBridge uses pyodbc. ODBC Driver 17 for SQL Server (or later) must be installed on the MigrationBridge host. Verify:
python -c "import pyodbc; print([x for x in pyodbc.drivers() if 'SQL Server' in x])"
# Expected output includes: ['ODBC Driver 17 for SQL Server']
Download: Microsoft ODBC Driver for SQL Server
Pre-Flight Space Check
-- Check free space in the target database data files
SELECT
name AS logical_file,
CAST(size / 128.0 AS DECIMAL(10,2)) AS allocated_MB,
CAST(FILEPROPERTY(name, 'SpaceUsed') / 128.0 AS DECIMAL(10,2)) AS used_MB,
CAST((size - FILEPROPERTY(name, 'SpaceUsed')) / 128.0 AS DECIMAL(10,2)) AS free_MB
FROM sys.database_files
WHERE type_desc = 'ROWS';
Version Check
SELECT @@VERSION;
-- Minimum supported: SQL Server 2016 (13.x)
-- SQL Server 2016+ required for JSON_VALUE / OPENJSON support
-- SQL Server 2017+ recommended for STRING_AGG and improved JSON perf
JSON_VALUE, OPENJSON, JSON_QUERY) are not available. Columns holding nested objects and arrays will be stored as plain NVARCHAR(MAX) strings but cannot be queried with native JSON path syntax. Upgrade to SQL Server 2016+ before migrating if JSON querying is required.Configure Connections
Open MigrationBridge and navigate to the Connections tab. Configure the source (MongoDB) and target (SQL Server) connection profiles.
Source Connection — MongoDB
Target Connection — SQL Server
mongodb://localhost:27017). Entering a value in the Port field has no effect and does not override the URI port.Assessment — Schema Inference
Click Run Assessment on the Assessment tab. MigrationBridge connects to MongoDB, iterates each collection, and samples up to the first 1,000 documents to build a candidate SQL schema. No data is written to SQL Server at this stage.
How Schema Inference Works
- MigrationBridge scans the first 1,000 documents per collection (configurable via
migrationbridge.confkeyschema_sample_size). - For each field encountered, the BSON type is mapped to a SQL Server type (see Type Conversions).
- If a field appears in fewer than 50% of sampled documents, the resulting column is marked
NULL(nullable). Fields present in ≥50% are inferred asNOT NULLunless a null value is observed. - The widest compatible type wins when a field holds mixed types across documents (e.g., some documents have
Int32, others haveStringfor the same field → column becomesNVARCHAR(MAX)).
Field Name Sanitization
MongoDB allows field names that are not valid SQL Server identifiers. MigrationBridge applies the following sanitization rules automatically:
| Character | Replaced With | Example |
|---|---|---|
$ (dollar sign) | _ (underscore) | $type → _type |
. (dot / period) | _ (underscore) | address.city → address_city |
| Reserved SQL keywords | Column auto-quoted with [ ] | select → [select] |
_id Field Handling
The _id field is excluded automatically from schema inference and from the migration. It is never written to SQL Server. A new surrogate key column _row_id INT IDENTITY(1,1) NOT NULL PRIMARY KEY is added to every table instead.
Select Collections
On the Select Objects tab, choose which collections to migrate. Each collection maps to a SQL Server table of the same name under schema dbo.
Collections — AdventureWorks2019 (70 total)
| Collection | Est. Docs | Target Table | Status | |
|---|---|---|---|---|
| Product | 504 | dbo.Product |
Ready | |
| SalesOrderHeader | 31,465 | dbo.SalesOrderHeader |
Ready | |
| SalesOrderDetail | 121,317 | dbo.SalesOrderDetail |
Ready | |
| Customer | 19,820 | dbo.Customer |
Ready | |
| Person | 19,972 | dbo.Person |
Ready | |
| ErrorLog | 0 | dbo.ErrorLog |
Empty | |
| … 64 more collections selected … | ||||
Re-Run Behaviour
TRUNCATE TABLE before inserting rows. This avoids primary key conflicts from the IDENTITY surrogate key and ensures the target is an exact replica of the source at run time. If you need to preserve existing target rows (e.g., incremental append), disable this option in Settings → Migration Defaults.Run Migration
Click Start Migration on the Migration tab. MigrationBridge processes each selected collection sequentially: schema inference → CREATE TABLE → bulk insert. Progress is shown in real time in the log panel.
IDENTITY_INSERT Handling
MigrationBridge creates tables with an INT IDENTITY(1,1) surrogate key. Row values are inserted with IDENTITY_INSERT OFF (the default), meaning SQL Server auto-assigns the _row_id. The tool does not use SET IDENTITY_INSERT ON — there are no source identity values to preserve since _id is excluded.
C:\MigrationT\migration_logs\ with a timestamped filename. Keep this log for audit purposes, especially in regulated healthcare environments.Verify
After migration completes, validate row counts match the MongoDB source, run integrity checks, and spot-check field mapping on a sample of documents.
Row Count Validation
-- Quick row counts for all migrated tables
SELECT
t.name AS table_name,
p.rows AS row_count
FROM sys.tables t
JOIN sys.indexes i ON t.object_id = i.object_id AND i.index_id <= 1
JOIN sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id
WHERE t.is_ms_shipped = 0
ORDER BY p.rows DESC;
-- Total row count across all user tables
SELECT SUM(p.rows) AS total_rows
FROM sys.tables t
JOIN sys.indexes i ON t.object_id = i.object_id AND i.index_id <= 1
JOIN sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id
WHERE t.is_ms_shipped = 0;
-- Expected for AdventureWorks2019: 759,240
DBCC CHECKDB
-- Run integrity check on the target database (read-only, no repair)
DBCC CHECKDB (N'AdventureWorks2019') WITH NO_INFOMSGS, ALL_ERRORMSGS;
-- A clean run returns: "DBCC execution completed. If DBCC printed error messages, contact your system administrator."
-- No error messages = pass.
Spot-Check: Document Field Mapping
-- Compare a known document from MongoDB against its SQL row
-- MongoDB source (mongosh):
-- db.Product.findOne({ Name: "Mountain-100 Black, 38" })
-- SQL Server target:
SELECT TOP 5 *
FROM dbo.Product
WHERE Name = 'Mountain-100 Black, 38';
-- Verify nested JSON field (if ProductModel was a sub-document):
SELECT
Name,
JSON_VALUE(ProductModel, '$.Name') AS model_name,
JSON_VALUE(ProductModel, '$.ModifiedDate') AS model_modified
FROM dbo.Product
WHERE ProductModel IS NOT NULL;
db.collection.countDocuments({})) against SQL Server row counts. For AdventureWorks2019, SalesOrderDetail (121,317 rows) and TransactionHistory (113,443 rows) are good high-volume spot-check targets.Post-Migration Hardening
MigrationBridge creates tables and loads data but does not create indexes, foreign keys, or statistics. These must be added manually after migration for production use.
Add Indexes
-- Example: non-clustered index on a high-cardinality column
CREATE NONCLUSTERED INDEX IX_SalesOrderHeader_CustomerID
ON dbo.SalesOrderHeader (CustomerID)
INCLUDE (OrderDate, TotalDue);
-- Example: index for Order Detail lookup by header
CREATE NONCLUSTERED INDEX IX_SalesOrderDetail_SalesOrderID
ON dbo.SalesOrderDetail (SalesOrderID)
INCLUDE (ProductID, OrderQty, UnitPrice);
UPDATE STATISTICS
-- Update statistics on all user tables after bulk load
EXEC sp_updatestats;
-- Or target a specific table:
UPDATE STATISTICS dbo.SalesOrderDetail WITH FULLSCAN;
JSON Path Index (SQL Server 2016+)
Columns holding serialised nested documents or arrays (NVARCHAR(MAX) JSON strings) benefit from computed column indexes. Use this pattern to index a specific JSON path for fast lookups:
-- Step 1: Add a computed column that extracts the JSON path value
ALTER TABLE dbo.Product
ADD ProductModel_Name AS JSON_VALUE(ProductModel, '$.Name');
-- Step 2: Create an index on the computed column
CREATE NONCLUSTERED INDEX IX_Product_ModelName
ON dbo.Product (ProductModel_Name)
WHERE ProductModel_Name IS NOT NULL;
-- Now this query uses the index:
SELECT * FROM dbo.Product WHERE JSON_VALUE(ProductModel, '$.Name') = 'HL Road Frame';
JSON_VALUE to extract scalar values (strings, numbers, booleans) from JSON columns. Use JSON_QUERY to extract sub-objects or arrays. Use OPENJSON to shred a JSON array into rows. All three require SQL Server 2016 (compatibility level 130) or higher.Post-Migration Checklist
| Task | Command / Method | Status |
|---|---|---|
| Row count validation | See Step 5 | Required |
| DBCC CHECKDB | See Step 5 | Required |
| Add non-clustered indexes | Manual DDL per workload | Required |
| UPDATE STATISTICS | sp_updatestats | Required |
| JSON path computed columns | Manual — JSON columns only | Recommended |
| Foreign key constraints | Manual DDL | Recommended |
| Application connection string update | App config | Required |
| Query plan review (Query Store) | SQL Server Query Store | Recommended |
Live Change Streams — MongoDB → SQL Server
MigrationBridge supports continuous replication from MongoDB to SQL Server using MongoDB Change Streams. This enables near-real-time synchronisation after the initial full load completes.
mongod instances do not support Change Streams. Verify with rs.status() before enabling live replication.Full-Load-First Sequence (Critical Order)
The correct sequence for zero-data-loss live replication is:
- Save the current oplog LSN (resume token) before the first row is inserted into SQL Server.
- Run the full load — migrate all collections to SQL Server.
- Start tailing the Change Stream from the saved resume token. This ensures no changes made during the full load are missed.
MigrationBridge handles this sequence automatically when Live Replication mode is selected. Do not start the Change Stream consumer manually before the full load completes — doing so risks missing or double-applying changes.
Oplog Retention: 30-Day Window
For reliable Change Stream resumability (e.g., after a MigrationBridge restart), configure a minimum 30-day oplog retention:
// MongoDB 4.4+ -- resize oplog to 50 GB and enforce 720-hour (30-day) minimum retention
db.adminCommand({
replSetResizeOplog: 1,
size: 51200 // size in MB (50 GB)
})
db.adminCommand({
replSetResizeOplog: 1,
minRetentionHours: 720 // 30 days = 720 hours
})
// Verify
db.adminCommand({ replSetGetConfig: 1 }).config.settings
minRetentionHours parameter requires Atlas M30+ tier or higher. On M10/M20, oplog window may be shorter than 30 days under high write throughput.DML Replication: INS / UPD / DEL
MigrationBridge tails the Change Stream and applies the following operations to SQL Server in real time:
| MongoDB Change Event | SQL Server DML Applied | Notes |
|---|---|---|
insert |
INSERT INTO dbo.<table> |
New document fields mapped to columns; extra fields not in schema silently ignored |
update |
UPDATE dbo.<table> SET ... WHERE _row_id = ? |
Matched on internal tracking key; updated fields only |
replace |
UPDATE dbo.<table> SET ... WHERE _row_id = ? |
Full document replacement treated as UPDATE |
delete |
DELETE FROM dbo.<table> WHERE _row_id = ? |
Hard delete; soft-delete pattern not applied |
drop |
No action | Collection drop does not drop the SQL table; logged as warning |
Validation Test
// MongoDB (mongosh) -- insert a test document
db.Product.insertOne({
Name: "_MigrationBridge_Test",
ProductNumber: "MB-TEST-001",
ListPrice: 0.00
})
// Wait ~2-5 seconds, then check SQL Server:
SELECT * FROM dbo.Product WHERE Name = '_MigrationBridge_Test';
-- Expected: 1 row returned
// Update in MongoDB:
db.Product.updateOne(
{ Name: "_MigrationBridge_Test" },
{ $set: { ListPrice: 9.99 } }
)
// Verify update replicated:
SELECT ListPrice FROM dbo.Product WHERE Name = '_MigrationBridge_Test';
-- Expected: 9.99
// Cleanup:
db.Product.deleteOne({ Name: "_MigrationBridge_Test" })
DELETE FROM dbo.Product WHERE Name = '_MigrationBridge_Test';
Type Conversions
The following table shows how MigrationBridge maps MongoDB BSON types to SQL Server column types during schema inference and data load.
| MongoDB / BSON Type | SQL Server Type | Notes |
|---|---|---|
Int32 |
INT |
32-bit signed integer |
Int64 |
BIGINT |
64-bit signed integer |
Double |
FLOAT |
IEEE 754 double-precision; use DECIMAL/NUMERIC manually if precision critical |
Decimal128 |
DECIMAL(38,10) |
High-precision monetary/numeric values |
Boolean |
BIT |
true → 1, false → 0 |
Date (ISODate) |
DATETIME2 |
Full UTC timestamp; DATETIME2 supports full MongoDB date range |
String |
NVARCHAR(MAX) |
Unicode; length not inferred — always MAX for safety |
ObjectId |
VARCHAR(24) |
24-character hex string representation |
Array |
NVARCHAR(MAX) JSON string |
Serialised as JSON array; query with OPENJSON |
Object (nested document) |
NVARCHAR(MAX) JSON string |
Serialised as JSON object; query with JSON_VALUE / JSON_QUERY |
BinData |
VARBINARY(MAX) |
Raw binary data; subtype metadata lost |
Null |
NULL |
Explicit null written as SQL NULL |
Undefined |
NULL |
Treated as null (deprecated BSON type) |
Timestamp (internal) |
BIGINT |
MongoDB internal replication timestamp; stored as raw int64 |
Symbol |
NVARCHAR(MAX) |
Deprecated BSON type; treated as string |
_id (any type) |
Excluded | Never migrated. Replaced by _row_id INT IDENTITY(1,1) PRIMARY KEY |
Double maps to SQL Server FLOAT which is an approximate numeric type. If you are migrating financial data (prices, amounts), alter the column to DECIMAL(18,4) or DECIMAL(38,10) after migration and convert the data. Do not rely on FLOAT for monetary calculations.Known Issues & Limitations
General
| Issue | Detail | Workaround |
|---|---|---|
_id excluded |
The MongoDB _id field is never migrated. References between collections via _id are broken post-migration. |
Export _id to a separate VARCHAR(24) column via a pre-migration script if cross-collection references must be preserved. |
| Nested / array structure lost | Nested documents and arrays are stored as flat JSON strings in NVARCHAR(MAX) columns. Hierarchical relationships are not normalised into child tables. |
Use OPENJSON / JSON_VALUE for querying, or write post-migration normalisation scripts to shred JSON into relational tables. |
| Field name conflicts with SQL reserved words | Fields named select, order, table, etc. are auto-quoted as [select], [order], [table]. Applications must use quoted identifiers when querying these columns. |
Review the sanitization warnings in the migration log and rename columns post-migration if quoting is problematic for your application. |
| Schema drift | Collections where documents have significantly different field sets (sparse or versioned schemas) may result in incomplete schema inference from the 1,000-doc sample. | Increase schema_sample_size in migrationbridge.conf (e.g., 5000). Review inferred DDL in Assessment Results before migrating. |
| Mixed types in same field | If a field holds Int32 in some docs and String in others, MigrationBridge widens to NVARCHAR(MAX). Numeric operations on that column will require CAST/TRY_CAST. |
Clean up type inconsistencies in MongoDB before migration, or add computed columns post-migration for typed access. |
Cosmos DB Mongo API
| Limitation | Detail |
|---|---|
No $lookup |
Cosmos Mongo API does not support the $lookup aggregation stage. Assessment queries that use $lookup will fail. MigrationBridge does not use $lookup internally, but custom pre-migration scripts may need to be adjusted. |
| No multi-document transactions | Cosmos Mongo API does not support multi-document ACID transactions. Change Stream consistency guarantees differ from native MongoDB. |
| Limited operator support | Several MongoDB aggregation operators are not supported in Cosmos Mongo API. Review the Cosmos DB MongoDB 4.2 feature support matrix before migrating. |
| Change Streams via Cosmos | Change Streams are supported on Cosmos Mongo API but have higher latency than native MongoDB. The resume token format differs — do not mix resume tokens between native MongoDB and Cosmos Mongo API instances. |
Atlas
| Limitation | Detail |
|---|---|
| 30-day oplog window requires M30+ | On Atlas M10 and M20 tiers, the oplog window may be shorter than 30 days under high write loads. Use M30+ for reliable 30-day Change Stream resumability. |
| IP access list | The MigrationBridge host IP must be in the Atlas cluster's IP access list. For Azure-hosted MigrationBridge, use a static outbound IP or VNet Peering with Atlas private endpoint. |
UI Busy / Not Responding
During high-throughput bulk copy operations, the MigrationBridge UI may display "Not Responding" in the Windows title bar. This is expected behaviour and does not indicate a crash or error. The migration continues running on a background thread; the UI thread is temporarily blocked by dashboard refresh polling.
How to Confirm the Migration Is Still Running
- Open Task Manager (Ctrl+Shift+Esc) → Details tab. Look for
python.exe— if CPU usage is above 0%, the migration is active. - Tail the live log file at
C:\MigrationT\migration_logs\— new lines are written every few seconds during active migration. - Check SQL Server row counts on the target table to confirm rows are being inserted.
Fix: Change 143
This behaviour was addressed in MigrationBridge Change 143 (included in v1.3.0). The fix implements two improvements:
- Reduced poll batch size — the background thread now processes rows in smaller batches, yielding the GIL more frequently and allowing the UI thread to update between batches.
- Deferred dashboard refresh — the live statistics panel (rows/sec, progress bar) now updates every 2 seconds instead of after every batch commit, reducing UI thread contention during sustained high-throughput copy.
If you are running a version prior to v1.3.0, upgrade to resolve the Not Responding behaviour. Upgrade instructions: User Guide → Upgrading MigrationBridge.
python -m migrationbridge --config migrationbridge.conf --run) rather than the GUI. The CLI mode has no UI refresh overhead and is significantly faster for bulk operations.MigrationBridge v1.3.0 · MongoDB → SQL Server · User Guide · Troubleshooting