PostgreSQL から SQL Server に Django アプリを移行する

この記事は、PostgreSQL (psycopg2 または psycopg) から SQL Server (mssql-django) に移行する Django アプリケーションの詳細な移行ガイドです。 任意のデータベースからの移行の一般的な概要については、他のデータベースから SQL Server への Django アプリの移行に関するページを参照してください。

Prerequisites

  • Python 3.8 以降
  • SQL Server 用 Microsoft ODBC Driver 17 または 18 mssql-django のインストールを参照してください。
  • SQL Server 2016 以降、またはAzure SQL Database

データベース バックエンドを切り替える

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では機能しません。 次のセクションでは、各機能を置き換える方法について説明します。

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 の範囲の種類 (IntegerRangeFieldBigIntegerRangeFieldDateRangeFieldDateTimeRangeFieldDecimalRangeField) には、同等のSQL Serverはありません。 2 つの異なるフィールドを使用します。

# 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に 2 つの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_AGG2017 以降SQL ServerまたはAzure SQL Databaseが必要です。

フルテキスト検索の移行

PostgreSQL フルテキスト検索では、 tsvectortsquery、および GIN インデックスが使用されます。 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_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 Serverの geography 列または geometry 列にマップされる生の SQL フィールドまたはカスタム モデル フィールドを使用して空間データを格納します。
  • SQL Server に組み込まれている空間関数と生の SQL を使用して空間データを照会する:
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空間サポートを追加するサード パーティ製ライブラリを検討するか、他のすべての場合に ORM を使用しながら空間クエリを生 SQL として保持します。

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 では、グループごとに 1 行を取得する 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では、2016 年SQL Server以降に使用可能なクエリ関数を使用して、JSON を nvarchar(max) として格納します。

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_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.postgresINSTALLED_APPSからsettings.pyを削除します。

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

移行チェックリスト

Step 詳細
バックエンドの切り替え django.db.backends.postgresqlmssql 内のsettings.pyに置き換えます。
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 を有効にする PostgreSQL MVCC の動作と一致するように READ_COMMITTED_SNAPSHOT ON を設定します。 トランザクション分離の違いを参照してください。
照合のテスト 大文字と小文字の区別の動作が期待値と一致するかどうかを確認します。 照合順序の違いを参照してください。
psycopg2 を削除する psycopg2-binaryまたはpsycopgをアンインストールします。 django.contrib.postgres を削除します。
マイグレーションを再生成する 古い移行ファイルを削除し、makemigrationsmigrate をクリーンな状態で新たに実行します。
データの移行 大規模なデータセットには、 dumpdata/loaddata または ETL ツールを使用します。