Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
This article provides guidance for migrating Django applications from PostgreSQL, MySQL, or SQLite to SQL Server using the mssql-django backend.
Overview
Django's ORM abstracts most database differences, but some behaviors and SQL dialects vary between backends. This guide covers the key differences you encounter when migrating to SQL Server.
Step 1: Install mssql-django
Install the mssql-django package and its dependencies:
pip install mssql-django
Ensure the Microsoft ODBC Driver for SQL Server is installed. See Install mssql-django for platform-specific instructions.
Step 2: Update DATABASE configuration
Replace your existing database configuration in settings.py:
# Example: From PostgreSQL
# DATABASES = {
# "default": {
# "ENGINE": "django.db.backends.postgresql",
# "NAME": "mydb",
# },
# }
# To SQL Server
DATABASES = {
"default": {
"ENGINE": "mssql",
"NAME": "<your-database>",
"USER": "<your-username>",
"PASSWORD": "<your-password>",
"HOST": "<your-server>",
"PORT": "1433",
"OPTIONS": {
"driver": "ODBC Driver 18 for SQL Server",
},
},
}
Step 3: Create fresh migrations
Start with a clean migration history for SQL Server:
# Remove existing migration files (keep __init__.py)
# Then regenerate
python manage.py makemigrations
python manage.py migrate
Important
Transfer your data using a data migration or separate ETL process. Don't attempt to run PostgreSQL or MySQL migration files against SQL Server.
Key differences from PostgreSQL
| Feature | PostgreSQL | SQL Server (mssql-django) |
|---|---|---|
| Auto-increment | SERIAL / BIGSERIAL |
IDENTITY(1,1) |
| Boolean type | Native boolean |
bit (0 or 1) |
| Text fields | text (unlimited) |
nvarchar(max) |
| JSON support | Native jsonb |
nvarchar(max) with JSON functions (SQL Server 2016+) |
| Array fields | ArrayField |
Not supported. Use a related table or JSON. |
| HStore fields | HStoreField |
Not supported. Use JSONField instead. |
| Range fields | IntegerRangeField, BigIntegerRangeField, DateRangeField, DateTimeRangeField |
Not supported. Use two separate fields. |
| Full-text search | SearchVector, SearchRank |
Use raw SQL with SQL Server full-text search. |
DISTINCT ON |
Supported | Not supported. Use GROUP BY or subqueries. |
DateTimeField with time zone |
timestamp with time zone |
datetimeoffset (when USE_TZ=True) or datetime2 |
PostgreSQL-specific features to replace
If your code uses PostgreSQL-specific features from django.contrib.postgres, replace them:
# PostgreSQL ArrayField - replace with JSONField or related table
# Before
from django.contrib.postgres.fields import ArrayField
tags = ArrayField(models.CharField(max_length=50))
# After (using JSONField)
tags = models.JSONField(default=list)
# PostgreSQL HStoreField - replace with JSONField
# Before
from django.contrib.postgres.fields import HStoreField
metadata = HStoreField()
# After
metadata = models.JSONField(default=dict)
Key differences from MySQL
| Feature | MySQL | SQL Server (mssql-django) |
|---|---|---|
| Auto-increment | AUTO_INCREMENT |
IDENTITY(1,1) |
| Boolean type | tinyint(1) |
bit |
| Text fields | longtext |
nvarchar(max) |
| JSON support | Native JSON (5.7 and later) |
nvarchar(max) with JSON functions |
| Collation | Per-column configurable | Instance or database level (override with COLLATE option) |
DateTimeField |
datetime(6) | datetimeoffset or datetime2 |
Key differences from SQLite
| Feature | SQLite | SQL Server (mssql-django) |
|---|---|---|
| Type enforcement | Flexible typing | Strict type enforcement |
| Concurrent writes | Limited | Full concurrency support |
| Maximum connections | Effectively 1 writer | Connection pooling with many concurrent connections |
DateTimeField |
Stored as text | datetimeoffset or datetime2 |
Collation differences
Collation controls how SQL Server compares and sorts text. This is one of the most common sources of unexpected behavior when migrating from PostgreSQL or MySQL.
Case sensitivity
SQL Server's default collation (SQL_Latin1_General_CP1_CI_AS) is case-insensitive. PostgreSQL is case-sensitive by default.
This behavior means that after migration, queries that previously distinguished between "Smith" and "smith" treat them as equal:
# On PostgreSQL: returns only exact case matches
# On SQL Server (default collation): returns both "Smith" and "smith"
User.objects.filter(last_name="Smith")
If your application depends on case-sensitive comparisons, you have two options:
Change the database or column collation to a case-sensitive variant:
-- Database-level (affects all new columns) ALTER DATABASE [<your-database>] COLLATE Latin1_General_CS_AS; -- Column-level (for specific columns) ALTER TABLE [<your-table>] ALTER COLUMN [<column-name>] NVARCHAR (150) COLLATE Latin1_General_CS_AS;Use Django's
__exactlookup with a collation override in raw SQL for targeted queries.
Accent sensitivity
The default SQL Server collation is accent-sensitive (AS), which matches PostgreSQL's behavior. Characters like é and e are treated as different. If you need accent-insensitive comparisons, use a collation ending in _AI.
Configure collation in mssql-django
Override the default collation for text field lookups in your database configuration:
DATABASES = {
"default": {
"ENGINE": "mssql",
"NAME": "<your-database>",
"OPTIONS": {
"driver": "ODBC Driver 18 for SQL Server",
"collation": "Latin1_General_CS_AS", # Case-sensitive
},
},
}
Note
The collation option in mssql-django controls the collation used in LIKE and comparison operations generated by Django's ORM lookups. It doesn't change the collation of existing columns in the database. To change stored column collation, use ALTER TABLE / ALTER COLUMN statements. For more information, see SQL Server collation documentation.
Step 4: Update custom SQL
If your code contains raw SQL, update it for SQL Server syntax:
# PostgreSQL syntax
# cursor.execute("SELECT * FROM products LIMIT 10 OFFSET 20")
# SQL Server syntax
cursor.execute("SELECT * FROM products ORDER BY id OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY")
Common SQL syntax differences:
| Operation | PostgreSQL/MySQL | SQL Server |
|---|---|---|
| Limit results | LIMIT 10 |
TOP 10 or OFFSET ... FETCH NEXT ... |
| String concatenation | \|\| (PG) / CONCAT() |
+ or CONCAT() |
| Boolean literals | TRUE / FALSE |
1 / 0 |
| Current timestamp | NOW() |
GETDATE(), SYSDATETIME(), or SYSDATETIMEOFFSET() for time zone-aware values |
| IF NOT EXISTS | CREATE TABLE IF NOT EXISTS |
Check sys.objects or use IF NOT EXISTS |
Transaction isolation differences
PostgreSQL uses MVCC (Multi-Version Concurrency Control) for its READ COMMITTED isolation level. Readers never block writers and writers never block readers.
SQL Server's default READ COMMITTED uses locking, which means read queries can block while waiting for write transactions to complete. If your application experiences increased blocking after migration, consider enabling READ COMMITTED SNAPSHOT on the database:
ALTER DATABASE [<your-database>]
SET READ_COMMITTED_SNAPSHOT ON;
This changes SQL Server's READ COMMITTED to use row versioning (similar to PostgreSQL's MVCC) instead of locking. Readers see the last committed version of a row without waiting for active writers.
Note
READ COMMITTED SNAPSHOT requires additional tempdb space for row versions. Test under realistic load before enabling in production. For more information, see Transaction management in mssql-django.
Step 5: Migrate data
Data migration strategy depends on dataset size:
Small datasets (<500 MB)
Use Django's dumpdata/loaddata:
# On the source database
python manage.py dumpdata --natural-foreign --natural-primary -o data.json
# Switch settings.py to SQL Server, then:
python manage.py migrate
python manage.py loaddata data.json
Large datasets (>500 MB)
For large migrations, use specialized tools to avoid memory exhaustion and timeout issues. Django's ORM isn't the right tool for bulk loads at this scale. Bypass it for the data move and let Django manage schema and application logic afterward.
| Tool | Best for |
|---|---|
| SQL Server Import and Export Wizard | On-premises to on-premises migrations with a GUI |
| Azure Data Factory | Any source to Azure SQL, including hybrid scenarios |
| Azure Database Migration Service | Large-scale migrations with built-in validation and rollback |
| mssql-python bulk copy with Apache Arrow | Custom Python pipelines that need maximum throughput between SQL Server, Azure SQL Database, and SQL database in Fabric |
Post-migration validation
After migration, validate identity seed consistency for auto-increment columns:
-- Check identity seed and current value for all tables
SELECT
TABLE_NAME,
IDENT_SEED(TABLE_SCHEMA + '.' + TABLE_NAME) AS IdentitySeed,
IDENT_INCR(TABLE_SCHEMA + '.' + TABLE_NAME) AS IdentityIncrement,
IDENT_CURRENT(TABLE_SCHEMA + '.' + TABLE_NAME) AS CurrentIdentity
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_TYPE = 'BASE TABLE'
AND OBJECTPROPERTY(OBJECT_ID(TABLE_SCHEMA + '.' + TABLE_NAME), 'TableHasIdentity') = 1
ORDER BY TABLE_NAME;
If CurrentIdentity exceeds IdentitySeed + record_count, reseed:
DBCC CHECKIDENT ('your_table', RESEED, new_seed);