Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
В этой статье описано подробное руководство по миграции приложений Django, перемещающихся из PostgreSQL (psycopg2илиpsycopg) в SQL Server (mssql-django). Общие сведения о миграции из любой базы данных см. в статье "Миграция приложений Django из других баз данных в SQL Server".
Необходимые условия
- Python 3.10 по 3.14
- Поддерживаемый драйвер базы данных Python. См. Выберите драйвер базы данных и установите mssql-django.
- SQL Server 2017 и более поздние версии, База данных SQL Azure или SQL Database in Fabric
Создание базы данных 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 для больших наборов данных. |