فهرست منبع

Closes #18191: Remove duplicate SQL indexes (#19074)

* Closes #18191: Remove redundant SQL indexes

* Update developer documentation

* Add a system check for duplicate indexes
Jeremy Stretch 10 ماه پیش
والد
کامیت
67480dcf4f

+ 1 - 1
docs/development/extending-models.md

@@ -6,7 +6,7 @@ Below is a list of tasks to consider when adding a new field to a core model.
 
 
 Add the field to the model, taking care to address any of the following conditions.
 Add the field to the model, taking care to address any of the following conditions.
 
 
-* When adding a GenericForeignKey field, also add an index under `Meta` for its two concrete fields. For example:
+* When adding a GenericForeignKey field, you may need add an index under `Meta` for its two concrete fields. (This is required only for non-unique GFK relationships, as the unique constraint introduces its own index.) For example:
 
 
     ```python
     ```python
     class Meta:
     class Meta:

+ 1 - 0
netbox/core/apps.py

@@ -19,6 +19,7 @@ class CoreConfig(AppConfig):
 
 
     def ready(self):
     def ready(self):
         from core.api import schema  # noqa: F401
         from core.api import schema  # noqa: F401
+        from core.checks import check_duplicate_indexes  # noqa: F401
         from netbox.models.features import register_models
         from netbox.models.features import register_models
         from . import data_backends, events, search  # noqa: F401
         from . import data_backends, events, search  # noqa: F401
         from netbox import context_managers  # noqa: F401
         from netbox import context_managers  # noqa: F401

+ 41 - 0
netbox/core/checks.py

@@ -0,0 +1,41 @@
+from django.core.checks import Error, register, Tags
+from django.db.models import Index, UniqueConstraint
+from django.apps import apps
+
+__all__ = (
+    'check_duplicate_indexes',
+)
+
+
+@register(Tags.models)
+def check_duplicate_indexes(app_configs, **kwargs):
+    """
+    Check for an index which is redundant to a declared unique constraint.
+    """
+    errors = []
+
+    for model in apps.get_models():
+        if not (meta := getattr(model, "_meta", None)):
+            continue
+
+        index_fields = {
+            tuple(index.fields) for index in getattr(meta, 'indexes', [])
+            if isinstance(index, Index)
+        }
+        constraint_fields = {
+            tuple(constraint.fields) for constraint in getattr(meta, 'constraints', [])
+            if isinstance(constraint, UniqueConstraint)
+        }
+
+        # Find overlapping definitions
+        if duplicated := index_fields & constraint_fields:
+            for fields in duplicated:
+                errors.append(
+                    Error(
+                        f"Model '{model.__name__}' defines the same field set {fields} in both `Meta.indexes` and "
+                        f"`Meta.constraints`.",
+                        obj=model,
+                    )
+                )
+
+    return errors

+ 25 - 0
netbox/core/migrations/0014_remove_redundant_indexes.py

@@ -0,0 +1,25 @@
+# Generated by Django 5.2b1 on 2025-04-03 18:32
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0013_datasource_sync_interval'),
+    ]
+
+    operations = [
+        migrations.RemoveIndex(
+            model_name='autosyncrecord',
+            name='core_autosy_object__c17bac_idx',
+        ),
+        migrations.RemoveIndex(
+            model_name='datafile',
+            name='core_datafile_source_path',
+        ),
+        migrations.RemoveIndex(
+            model_name='managedfile',
+            name='core_managedfile_root_path',
+        ),
+    ]

+ 0 - 6
netbox/core/models/data.py

@@ -310,9 +310,6 @@ class DataFile(models.Model):
                 name='%(app_label)s_%(class)s_unique_source_path'
                 name='%(app_label)s_%(class)s_unique_source_path'
             ),
             ),
         )
         )
-        indexes = [
-            models.Index(fields=('source', 'path'), name='core_datafile_source_path'),
-        ]
         verbose_name = _('data file')
         verbose_name = _('data file')
         verbose_name_plural = _('data files')
         verbose_name_plural = _('data files')
 
 
@@ -387,8 +384,5 @@ class AutoSyncRecord(models.Model):
                 name='%(app_label)s_%(class)s_object'
                 name='%(app_label)s_%(class)s_object'
             ),
             ),
         )
         )
-        indexes = (
-            models.Index(fields=('object_type', 'object_id')),
-        )
         verbose_name = _('auto sync record')
         verbose_name = _('auto sync record')
         verbose_name_plural = _('auto sync records')
         verbose_name_plural = _('auto sync records')

+ 0 - 3
netbox/core/models/files.py

@@ -58,9 +58,6 @@ class ManagedFile(SyncedDataMixin, models.Model):
                 name='%(app_label)s_%(class)s_unique_root_path'
                 name='%(app_label)s_%(class)s_unique_root_path'
             ),
             ),
         )
         )
-        indexes = [
-            models.Index(fields=('file_root', 'file_path'), name='core_managedfile_root_path'),
-        ]
         verbose_name = _('managed file')
         verbose_name = _('managed file')
         verbose_name_plural = _('managed files')
         verbose_name_plural = _('managed files')
 
 

+ 17 - 0
netbox/dcim/migrations/0207_remove_redundant_indexes.py

@@ -0,0 +1,17 @@
+# Generated by Django 5.2b1 on 2025-04-03 18:32
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dcim', '0206_load_module_type_profiles'),
+    ]
+
+    operations = [
+        migrations.RemoveIndex(
+            model_name='cabletermination',
+            name='dcim_cablet_termina_884752_idx',
+        ),
+    ]

+ 0 - 3
netbox/dcim/models/cables.py

@@ -299,9 +299,6 @@ class CableTermination(ChangeLoggedModel):
 
 
     class Meta:
     class Meta:
         ordering = ('cable', 'cable_end', 'pk')
         ordering = ('cable', 'cable_end', 'pk')
-        indexes = (
-            models.Index(fields=('termination_type', 'termination_id')),
-        )
         constraints = (
         constraints = (
             models.UniqueConstraint(
             models.UniqueConstraint(
                 fields=('termination_type', 'termination_id'),
                 fields=('termination_type', 'termination_id'),

+ 21 - 0
netbox/vpn/migrations/0009_remove_redundant_indexes.py

@@ -0,0 +1,21 @@
+# Generated by Django 5.2b1 on 2025-04-03 18:32
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('vpn', '0008_add_l2vpn_status'),
+    ]
+
+    operations = [
+        migrations.RemoveIndex(
+            model_name='l2vpntermination',
+            name='vpn_l2vpnte_assigne_9c55f8_idx',
+        ),
+        migrations.RemoveIndex(
+            model_name='tunneltermination',
+            name='vpn_tunnelt_termina_c1f04b_idx',
+        ),
+    ]

+ 0 - 3
netbox/vpn/models/l2vpn.py

@@ -110,9 +110,6 @@ class L2VPNTermination(NetBoxModel):
 
 
     class Meta:
     class Meta:
         ordering = ('l2vpn',)
         ordering = ('l2vpn',)
-        indexes = (
-            models.Index(fields=('assigned_object_type', 'assigned_object_id')),
-        )
         constraints = (
         constraints = (
             models.UniqueConstraint(
             models.UniqueConstraint(
                 fields=('assigned_object_type', 'assigned_object_id'),
                 fields=('assigned_object_type', 'assigned_object_id'),

+ 0 - 3
netbox/vpn/models/tunnels.py

@@ -138,9 +138,6 @@ class TunnelTermination(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ChangeLo
 
 
     class Meta:
     class Meta:
         ordering = ('tunnel', 'role', 'pk')
         ordering = ('tunnel', 'role', 'pk')
-        indexes = (
-            models.Index(fields=('termination_type', 'termination_id')),
-        )
         constraints = (
         constraints = (
             models.UniqueConstraint(
             models.UniqueConstraint(
                 fields=('termination_type', 'termination_id'),
                 fields=('termination_type', 'termination_id'),