将 Django 应用从 PostgreSQL 迁移到 SQL Server

本文是有关从 PostgreSQL(或psycopg2)迁移到 SQL Server (psycopgmssql-django) 的 Django 应用程序的详细迁移指南。 有关从任何数据库迁移的一般概述,请参阅将 Django 应用从其他数据库迁移到SQL Server

先决条件

  • Python 3.8 或更高版本
  • 适用于 SQL Server 的 Microsoft ODBC Driver 17 或 18。 请参阅 安装 mssql-django
  • SQL Server 2016 或更高版本,或Azure SQL 数据库

切换数据库后端

settings.py 中替换你的 PostgreSQL 配置:

# Before (PostgreSQL)
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": "mydb",
        "USER": "myuser",
        "PASSWORD": "mypassword",
        "HOST": "localhost",
        "PORT": "5432",
    },
}

# After (SQL Server)
DATABASES = {
    "default": {
        "ENGINE": "mssql",
        "NAME": "mydb",
        "USER": "myuser",
        "PASSWORD": "mypassword",
        "HOST": "localhost",
        "PORT": "1433",
        "OPTIONS": {
            "driver": "ODBC Driver 18 for SQL Server",
        },
    },
}

更新requirements.txt

# Remove
# psycopg2-binary>=2.9
# or psycopg[binary]>=3.1

# Add
mssql-django>=1.5

替换 django.contrib.postgres 功能

django.contrib.postgres 模块提供特定于 PostgreSQL 的字段、函数和查找。 这些不适用于SQL Server。 以下部分演示如何替换每个功能。

数组字段

PostgreSQL ArrayField 原生支持数组存储。 SQL Server没有数组列类型。

选项 1:JSONField (适用于 Django 3.2 及更高版本)

# Before
from django.contrib.postgres.fields import ArrayField

class Product(models.Model):
    tags = ArrayField(models.CharField(max_length=50), default=list)

# After
class Product(models.Model):
    tags = models.JSONField(default=list)

查询更改:

# Before (PostgreSQL)
Product.objects.filter(tags__contains=["sale"])
Product.objects.filter(tags__overlap=["sale", "new"])
Product.objects.filter(tags__len=3)

# After (SQL Server with JSONField)
# Use __contains for exact list matching
Product.objects.filter(tags__contains=["sale"])

# For overlap-style queries, use raw SQL
from django.db.models.expressions import RawSQL
Product.objects.filter(
    pk__in=RawSQL(
        """
        SELECT p.id FROM products_product p
        CROSS APPLY OPENJSON(p.tags) t
        WHERE t.value IN (%s, %s)
        """,
        ["sale", "new"],
    )
)

选项 2:相关表 (规范化、更适合大型数组或频繁筛选)

class Product(models.Model):
    name = models.CharField(max_length=200)

class ProductTag(models.Model):
    product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name="tags")
    tag = models.CharField(max_length=50, db_index=True)

    class Meta:
        unique_together = [("product", "tag")]

HStoreField

替换为 JSONField

# Before
from django.contrib.postgres.fields import HStoreField

class Profile(models.Model):
    metadata = HStoreField(default=dict)

# After
class Profile(models.Model):
    metadata = models.JSONField(default=dict)

JSONField 支持相同的键查找语法:

# Both backends support this
Profile.objects.filter(metadata__theme="dark")

范围字段

PostgreSQL 范围类型(IntegerRangeField、、BigIntegerRangeFieldDateRangeField、、DateTimeRangeFieldDecimalRangeField)没有等效SQL Server。 使用两个单独的字段:

# Before
from django.contrib.postgres.fields import DateRangeField

class Event(models.Model):
    dates = DateRangeField()

# After
class Event(models.Model):
    start_date = models.DateField()
    end_date = models.DateField()

更新查询,以使用独立的字段比较。 之前,使用 PostgreSQL 的 DateRangeField

from django.contrib.postgres.fields import DateRangeField
from psycopg2.extras import DateRange

Event.objects.filter(dates__contains=DateRange(start, end))

之后,SQL Server上有两DateField列:

from datetime import date

start = date(2026, 1, 1)
end = date(2026, 12, 31)

Event.objects.filter(start_date__lte=start, end_date__gte=end)

CITextField 和 CIEmailField

PostgreSQL 不区分大小写的文本类型使用 citext 扩展。 SQL Server 的默认排序规则(SQL_Latin1_General_CP1_CI_AS)默认不区分大小写,因此,标准的 CharFieldEmailField 的行为方式相同:

# Before
from django.contrib.postgres.fields import CITextField

class Tag(models.Model):
    name = CITextField(max_length=100)

# After - already case-insensitive with default SQL Server collation
class Tag(models.Model):
    name = models.CharField(max_length=100)

SearchVector、SearchQuery、SearchRank

PostgreSQL 全文搜索与 Django 深度集成。 SQL Server有自己的全文搜索引擎,但没有 Django ORM 集成。 请参阅本文后面的 全文搜索迁移

聚合函数

替换 PostgreSQL 专用聚合:

# Before
from django.contrib.postgres.aggregates import ArrayAgg, StringAgg

Product.objects.values("category").annotate(
    all_names=ArrayAgg("name"),
    name_list=StringAgg("name", delimiter=", "),
)

# After - use SQL Server equivalents via RawSQL
from django.db.models.expressions import RawSQL

Product.objects.values("category").annotate(
    name_list=RawSQL(
        "STRING_AGG(name, ', ') WITHIN GROUP (ORDER BY name)",
        [],
    ),
)

Note

STRING_AGG需要 SQL Server 2017 或更高版本或Azure SQL 数据库。

全文搜索迁移

PostgreSQL 全文搜索使用tsvectortsqueryGIN索引。 SQL Server具有单独的全文搜索引擎。

在SQL Server中启用全文搜索

-- Create a full-text catalog
CREATE FULLTEXT CATALOG [MyAppCatalog] AS DEFAULT;

-- Create a full-text index (table must have a unique index)
CREATE FULLTEXT INDEX ON [products_product]([name], [description])
KEY INDEX [PK_products_product]
WITH CHANGE_TRACKING AUTO;

在 Django 中进行全文搜索查询

使用原始 SQL 访问 SQL Server 的 CONTAINSFREETEXT 函数:

from django.db.models.expressions import RawSQL

# Equivalent of PostgreSQL SearchVector + SearchQuery
def search_products(query):
    return Product.objects.filter(
        pk__in=RawSQL(
            """
            SELECT p.id FROM products_product p
            WHERE CONTAINS((p.name, p.description), %s)
            """,
            [query],
        )
    )

对于排名结果(等效于 SearchRank):

def search_products_ranked(query):
    return Product.objects.raw(
        """
        SELECT p.*, ft.[RANK]
        FROM products_product p
        INNER JOIN CONTAINSTABLE(products_product, (name, description), %s) ft
            ON p.id = ft.[KEY]
        ORDER BY ft.[RANK] DESC
        """,
        [query],
    )

全文索引维护操作手册

规划迁移后SQL Server全文索引的维护:

  • 使用 CHANGE_TRACKING AUTO 获取准实时更新。
  • 使用 CHANGE_TRACKING MANUAL 进行批量加载窗口操作,然后执行一次完整填充。
  • 通过 sys.fulltext_indexessys.dm_fts_index_population 查看爬网状态和积压情况。

检查状态:

SELECT
    OBJECT_NAME(i.object_id) AS table_name,
    i.change_tracking_state_desc,
    i.has_crawl_completed,
    i.crawl_type_desc
FROM sys.fulltext_indexes AS i;

使用手动跟踪加载大型数据后:

ALTER FULLTEXT INDEX ON [products_product] START FULL POPULATION;

Tip

在低流量时段重新生成或重新填充全文索引。 大型表上的完整填充成本可能很高。

创建搜索管理器

将原始 SQL 包装在管理器中,以便进行干净访问:

class ProductSearchManager(models.Manager):
    def search(self, query):
        if not query:
            return self.none()
        return self.filter(
            pk__in=RawSQL(
                """
                SELECT p.id FROM products_product p
                WHERE CONTAINS((p.name, p.description), %s)
                """,
                [query],
            )
        )

class Product(models.Model):
    name = models.CharField(max_length=200)
    description = models.TextField()

    objects = ProductSearchManager()
    # Usage: Product.objects.search("mountain bike")

PostGIS 和空间数据

mssql-django 不包括 GeoDjango GIS 后端。 如果你的 PostgreSQL 应用程序通过 django.contrib.gis 使用 PostGIS,就无法将空间查询直接迁移到 SQL Server 上的 Django ORM。

SQL Server 原生支持 地理几何 数据类型。 迁移后处理空间数据:

  • 使用原始 SQL 或映射到 SQL Server 的 geographygeometry 列的自定义模型字段来存储空间数据
  • 使用原始 SQL 和 SQL Server 的内置空间函数查询空间数据
from django.db import connection

with connection.cursor() as cursor:
    cursor.execute(
        """
        SELECT id, name
        FROM stores
        WHERE location.STDistance(geography::Point(%s, %s, 4326)) <= %s
        """,
        [latitude, longitude, radius_meters],
    )
  • 考虑可为 Django 添加 SQL Server 空间支持的第三方库,或者将空间查询保留为原始 SQL,而其他所有操作则使用 ORM。

Note

如果应用程序严重依赖于 GeoDjango 空间查找,请仔细评估迁移成本。 将空间查询移动到原始 SQL 需要重写每个 GeoDjango 空间筛选器。

连接池迁移

如果您的 PostgreSQL 应用程序使用 pgbouncer 进行连接池化,请将其替换为 Django 的内置连接管理或 ODBC 连接池。

Django 连接复用

DATABASES = {
    "default": {
        "ENGINE": "mssql",
        "NAME": "<your-database>",
        "HOST": "<your-server>",
        "OPTIONS": {
            "driver": "ODBC Driver 18 for SQL Server",
        },
        "CONN_MAX_AGE": 600,  # Reuse connections for 10 minutes
        "CONN_HEALTH_CHECKS": True,  # Django 4.1+
    },
}

有关详细信息,请参阅 mssql-django 中的连接池

DISTINCT ON 替代方案

PostgreSQL 支持使用 DISTINCT ON,从每个组中获取一行。 SQL Server不支持此语法。 请改用窗口函数:

# Before (PostgreSQL)
Entry.objects.order_by("blog_id", "-pub_date").distinct("blog_id")

# After (SQL Server) - use raw SQL with ROW_NUMBER
Entry.objects.raw(
    """
    SELECT * FROM (
        SELECT *, ROW_NUMBER() OVER (PARTITION BY blog_id ORDER BY pub_date DESC) AS rn
        FROM blog_entry
    ) sub
    WHERE rn = 1
    """
)

JSONB 查询

PostgreSQL jsonb 的类型支持丰富的查询运算符。 SQL Server将 JSON 存储为 nvarchar(max),其中包含自 2016 SQL Server以来可用的查询函数。

Django 的 JSONField 查找语法适用于两个后端,用于基本操作:

# Works on both PostgreSQL and SQL Server
Config.objects.filter(data__settings__theme="dark")
Config.objects.filter(data__has_key="settings")

对于 Django 的 ORM 不支持的高级 JSON 查询,请使用 SQL Server 的 JSON_VALUEOPENJSON 函数:

from django.db.models.expressions import RawSQL

# Query nested JSON values
Config.objects.annotate(
    theme=RawSQL("JSON_VALUE(data, '$.settings.theme')", [])
).filter(theme="dark")

删除 PostgreSQL 依赖项

迁移后,从项目中删除 PostgreSQL 包:

pip uninstall psycopg2-binary psycopg2 psycopg

settings.py中,从INSTALLED_APPS中移除django.contrib.postgres

INSTALLED_APPS = [
    # Remove this line:
    # "django.contrib.postgres",
    "django.contrib.admin",
    "django.contrib.auth",
    # ...
]

迁移清单

Step 详细信息
切换后端 settings.py 中,将 django.db.backends.postgresql 替换为 mssql
替换 contrib.postgres 交换 ArrayFieldHStoreField范围字段和 CI 字段。
更新全文搜索 tsvector/tsquery迁移到 SQL Server CONTAINS/FREETEXT
更新空间查询 使用SQL Server空间函数将 GeoDjango 查找重写为原始 SQL。
取代 DISTINCT ON 使用 ROW_NUMBER() 窗口函数。
更新原始 SQL 将 PostgreSQL 语法(LIMIT、、||NOW())更改为SQL Server语法。 请参阅 更新自定义 SQL
启用 RCSI READ_COMMITTED_SNAPSHOT ON 设置为与 PostgreSQL MVCC 的行为一致。 请参阅 事务隔离差异
测试排序规则 验证区分大小写的行为是否符合预期。 请参阅 排序规则差异
删除 psycopg2 卸载 psycopg2-binarypsycopg。 删除 django.contrib.postgres
重新生成迁移 删除旧的迁移文件,运行 makemigrationsmigrate 刷新。
迁移数据 对大型数据集使用 dumpdata/loaddata 或 ETL 工具。