Перенос приложений Django из PostgreSQL в SQL Server

В этой статье описано подробное руководство по миграции приложений Django, перемещающихся из PostgreSQL (psycopg2илиpsycopg) в SQL Server (mssql-django). Общие сведения о миграции из любой базы данных см. в статье "Миграция приложений Django из других баз данных в SQL Server".

Необходимые условия

Создание базы данных SQL

Создайте или подключитесь к SQL-базе данных на одной из следующих платформ:

Смените серверную часть базы данных

Замените конфигурацию PostgreSQL в settings.py:

# 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. В следующих разделах показано, как заменить каждую функцию.

ArrayField

PostgreSQL ArrayField хранит массивы в собственном коде. SQL Server не имеет типа столбца массива.

Вариант 1: JSONField

# 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, BigIntegerRangeField, DateRangeField, DateTimeRangeField, DecimalRangeField) не имеют эквивалента в 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()

Обновите запросы для использования отдельных сравнений полей. Раньше, с DateRangeField PostgreSQL:

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

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

После этого с двумя DateField столбцами на SQL Server:

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) уже нечувствительно к регистру, поэтому стандартные CharField и EmailField ведут себя одинаково:

# 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)",
        [],
    ),
)

Миграция полнотекстового поиска

Полнотекстовый поиск PostgreSQL использует tsvectorи tsqueryGIN индексы. 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 CONTAINS и FREETEXT:

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_indexes и sys.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 использует PostGIS черезdjango.contrib.gis, вы не можете перенести пространственные запросы непосредственно в Django ORM на SQL Server.

SQL Server поддерживает типы данных geography и geometry в собственном коде. Чтобы работать с пространственными данными после миграции:

  • Хранение пространственных данных с помощью необработанных полей SQL или настраиваемых моделей, которые сопоставляют с географическими или геометрическими столбцами SQL Server.
  • Запрос пространственных данных с помощью необработанных функций 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],
    )
  • Рассмотрите сторонние библиотеки, которые добавляют поддержку пространственных данных SQL Server в Django, или используйте пространственные запросы в виде raw 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,
    },
}

Дополнительные сведения см. в разделе "Пул подключений" в 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) и предоставляет встроенные функции запросов JSON.

Синтаксис поиска Django JSONField работает в обоих бэкендах при выполнении базовых операций:

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

Для сложных запросов JSON, не поддерживаемых ORM Django, используйте функции SQL Server JSON_VALUE и OPENJSON:

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

Удалить django.contrib.postgres из INSTALLED_APPS в settings.py:

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

Контрольный список миграции

Step Details
Переключить бэкенд Замените django.db.backends.postgresql на mssql в settings.py.
Заменить contrib.postgres Поменяйте местами ArrayField, HStoreField, поля диапазона и поля CI.
Обновление полнотекстового поиска Переход с tsvector/tsquery на SQL Server CONTAINS/FREETEXT.
Обновление пространственных запросов Перепишите запросы GeoDjango в raw SQL с использованием пространственных функций SQL Server.
Заменить DISTINCT ON Используйте ROW_NUMBER() функции окна.
Обновление необработанного SQL Измените синтаксис PostgreSQL (LIMIT, ||, NOW()) на SQL Server синтаксис. См. раздел "Обновление пользовательского SQL".
Включение RCSI Задайте READ_COMMITTED_SNAPSHOT ON для соответствия поведению PostgreSQL MVCC. См. различия изоляции транзакций.
Проверка сортировки Убедитесь, что поведение чувствительности к регистру соответствует вашим ожиданиям. См. различия в сортировке.
Удалить psycopg2 Удалите psycopg2-binary или psycopg. Удалите django.contrib.postgres.
Пересоздать миграции Удалите старые файлы миграции и заново выполните makemigrations и migrate.
Перенос данных Используйте dumpdata/loaddata или средство ETL для больших наборов данных.