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

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

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

  • Python 3.8 или более поздней версии
  • Microsoft драйвер ODBC 17 или 18 для SQL Server. См. раздел "Установка mssql-django".
  • SQL Server 2016 или более поздней версии или База данных SQL Azure

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

Замените конфигурацию 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 (работает с 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, 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)",
        [],
    ),
)

Note

STRING_AGGтребуется SQL Server 2017 или более поздней версии или База данных SQL Azure.

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

Полнотекстовый поиск 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,  # 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) с функциями запросов, доступными с SQL Server 2016 года.

Синтаксис поиска 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 для больших наборов данных.