將 Django 應用程式從其他資料庫遷移到 SQL Server

本文提供使用 mssql-django 後端將 Django 應用程式從 PostgreSQL、MySQL 或 SQLite 遷移至 SQL Server 的指引。

Overview

Django 的 ORM 抽象化了大部分資料庫差異,但後端間的行為和 SQL 方言有所不同。 本指南涵蓋你在遷移到 SQL Server 時遇到的主要差異。

步驟 1:安裝 mssql-django

安裝 mssql-django 套件及其相依性:

pip install mssql-django

請確保已安裝 Microsoft ODBC 的 SQL Server 驅動程式。 請參閱 Install mssql-django 以了解平台專屬的說明。

步驟 2:更新 DATABASE 設定

在 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",
        },
    },
}

步驟 3:建立新的遷移

先從乾淨的 SQL Server 遷移歷史開始:

# Remove existing migration files (keep __init__.py)
# Then regenerate
python manage.py makemigrations
python manage.py migrate

Important

使用資料遷移或獨立的ETL流程來轉移資料。 不要嘗試在 SQL Server 上執行 PostgreSQL 或 MySQL 的遷移檔案。

與 PostgreSQL 的主要差異

Feature PostgreSQL SQL Server (mssql-django)
自動遞增 SERIAL / BIGSERIAL IDENTITY(1,1)
布林型 原生 boolean 位元 (0 或 1)
文字欄位 text (無限) nvarchar(max)
JSON 支援 原生 jsonb nvarchar(max) 搭配 JSON 函式 (SQL Server 2016+)
陣列欄位 ArrayField 不支援。 使用相關的資料表或 JSON。
HStore 欄位 HStoreField 不支援。 請改用 JSONField。
範圍欄位 IntegerRangeField、BigIntegerRangeField、DateRangeField、DateTimeRangeField 不支援。 使用兩個不同的欄位。
全文搜尋 SearchVector、SearchRank 使用原始 SQL 搭配 SQL Server 全文搜尋。
DISTINCT ON 支持 不支援。 使用 GROUP BY 或子查詢。
DateTimeField 含時區 timestamp with time zone datetimeoffset(當 USE_TZ=True 時)或 datetime2

要替換的 PostgreSQL 專屬功能

如果您的程式碼使用來自 django.contrib.postgres 的 PostgreSQL 專屬功能,請將其替換:

# 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)

與 MySQL 的主要差異

Feature MySQL SQL Server (mssql-django)
自動遞增 AUTO_INCREMENT IDENTITY(1,1)
布林型 tinyint(1) bit
文字欄位 longtext nvarchar(max)
JSON 支援 原生 JSON (5.7 及後續版本) nvarchar(max) 與 JSON 函數
Collation 每欄可配置 實例或資料庫層級(可用 COLLATE 選項覆寫)
DateTimeField 約會時間(6) Datetimeoffset 或 datetime2

與 SQLite 的主要差異

Feature SQLite SQL Server (mssql-django)
型別強制執行 彈性型別 嚴格執行型別限制
同時寫入 受限 全並行支援
連線數目上限 實際上只有一位作家 多個並行連線的連線池
DateTimeField 以文字儲存 Datetimeoffset 或 datetime2

排序差異

定序會控制 SQL Server 如何比較及排序文字資料。 這是從 PostgreSQL 或 MySQL 遷移時最常見的意外行為來源之一。

區分大小寫

SQL Server 的預設定序(SQL_Latin1_General_CP1_CI_AS)是不區分大小寫。 PostgreSQL 預設是區分大小寫的。

此行為表示,遷移後,先前會區分 "Smith" 與 "smith" 的查詢會將兩者視為相等:

# On PostgreSQL: returns only exact case matches
# On SQL Server (default collation): returns both "Smith" and "smith"
User.objects.filter(last_name="Smith")

如果您的應用程式依賴區分大小寫的比較,您有兩個選項:

  • 將資料庫或資料行的定序變更為區分大小寫的變體:

    -- 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;
    
  • 在原始 SQL 中使用 Django 的 __exact 查找並搭配定序覆寫,以執行針對性的查詢。

區分重音符號

SQL Server 預設的定序為區分重音(AS),這與 PostgreSQL 的行為一致。 像 é 和 e 這樣的字元會被視為不同。 如果您需要不區分重音符號的比較,請使用以 _AI 結尾的定序。

在 mssql-django 中設定定序

在您的資料庫設定中,覆寫預設的文字欄位查詢排序:

DATABASES = {
    "default": {
        "ENGINE": "mssql",
        "NAME": "<your-database>",
        "OPTIONS": {
            "driver": "ODBC Driver 18 for SQL Server",
            "collation": "Latin1_General_CS_AS",  # Case-sensitive
        },
    },
}

Note

mssql-django 中的 collation 選項可控制由 Django 的 ORM 查詢產生、用於 LIKE 和比較運算的定序規則。 它不會改變資料庫中現有欄位的整合。 要更改儲存的欄位排序,請使用 ALTER TABLE / ALTER COLUMN 陳述式。 欲了解更多資訊,請參閱 SQL Server 整合文件。

步驟 4:更新自訂 SQL

如果你的程式碼包含原始 SQL,請更新 SQL Server 語法:

# 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")

常見的 SQL 語法差異:

運算 PostgreSQL/MySQL SQL Server
限制結果 LIMIT 10 TOP 10 或 OFFSET ... FETCH NEXT ...
字串串連 \|\| (PG) / CONCAT() + 或 CONCAT()
布林字面值 TRUE / FALSE 1 / 0
目前時間戳記 NOW() GETDATE()、SYSDATETIME() 或 SYSDATETIMEOFFSET(),用於時區感知值
如果不存在 CREATE TABLE IF NOT EXISTS 查看 sys.objects 或使用 IF NOT EXISTS

交易隔離差異

PostgreSQL 採用 MVCC(多版本並發控制)作為 READ COMMITTED 隔離層級。 讀者從不封鎖寫者,寫者從不封鎖讀者。

SQL Server 預設READ COMMITTED使用鎖定機制,這表示讀取查詢可以在等待寫入交易完成時阻塞。 如果您的應用程式在遷移後出現更多阻擋,請考慮在資料庫啟用 READ COMMITTED SNAPSHOT :

ALTER DATABASE [<your-database>]
SET READ_COMMITTED_SNAPSHOT ON;

這會READ COMMITTED讓 SQL Server 改用列版本控制(類似 PostgreSQL 的 MVCC)取代鎖定。 讀者在不等待活躍寫手的情況下,就能看到一列最後一個已提交的版本。

Note

READ COMMITTED SNAPSHOT 需要額外的 tempdb 空間來儲存資料列版本。 在生產前,先在實際負載下測試。 欲了解更多資訊,請參閱 mssql-django 中的交易管理。

步驟五:資料遷移

資料遷移策略依資料集大小而定:

小型資料集(<500 MB)

使用 Django 的 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

大型資料集(>500 MB)

對於大型遷移,請使用專門工具以避免記憶體耗盡與逾時問題。 Django 的 ORM 並不是在這種規模下進行大量資料載入的合適工具。 在資料移動時繞過它,讓 Django 之後管理結構和應用邏輯。

Tool 最適合用於
SQL Server 匯入和匯出精靈 使用 GUI 的內部部署到內部部署遷移
Azure Data Factory 從任何來源到 Azure SQL,包括混合式情境
Azure 資料庫移轉服務 大規模遷移,內建驗證與回滾功能
使用 Apache Arrow 的 mssql-python 批量複製 自訂 Python 管線,需要在 SQL Server、Azure SQL Database 與 Fabric 中的 SQL 資料庫之間達到最大吞吐量

遷移後驗證

遷移後,驗證自動遞增欄位的身份種子一致性:

-- 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;

若 CurrentIdentity 超過 IdentitySeed + record_count,則重新設定種子:

DBCC CHECKIDENT ('your_table', RESEED, new_seed);