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

* Closes #18191: Remove redundant SQL indexes

* Update developer documentation

* Add a system check for duplicate indexes
This commit is contained in:
Jeremy Stretch 2025-04-03 16:16:57 -04:00 committed by GitHub
parent 6a966ee6c1
commit 67480dcf4f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 106 additions and 19 deletions

View File

@ -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:

View File

@ -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
netbox/core/checks.py Normal file
View File

@ -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

View File

@ -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',
),
]

View File

@ -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')

View File

@ -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')

View File

@ -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',
),
]

View File

@ -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'),

View File

@ -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',
),
]

View File

@ -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'),

View File

@ -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'),