Skip to content

Commit f38a77b

Browse files
nex2hexjeking3
andcommitted
Add index to history_date field to improve performance.
Opt-out of the index with `SETTINGS_HISTORY_DATE_INDEX=False`. Allow history_date indexing to be disabled or composite with model pk Co-authored-by: jeking3 <jim.king@cloudtruth.com>
1 parent b0b998c commit f38a77b

File tree

6 files changed

+113
-3
lines changed

6 files changed

+113
-3
lines changed

CHANGES.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ Changes
44
Unreleased
55
----------
66

7+
Upgrade Implications:
8+
9+
- Run `makemigrations` after upgrading to realize the benefit of indexing changes.
10+
11+
Full list of changes:
12+
13+
- Added index on `history_date` column; opt-out with setting `SIMPLE_HISTORY_DATE_INDEX` (gh-565)
714
- Fixed ``prev_record`` and ``next_record`` performance when using ``excluded_fields`` (gh-791)
815
- Fixed `update_change_reason` in pk (gh-806)
916
- Fixed bug where serializer of djangorestframework crashed if used with ``OrderingFilter`` (gh-821)

docs/historical_model.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,26 @@ model, will work too.
8686
my_poll.save()
8787
8888
89+
Indexed ``history_date``
90+
------------------------
91+
92+
Many queries use ``history_date`` as a filter. The as_of queries combine this with the
93+
original model's promary key to extract point-in-time snapshots of history. By default
94+
the ``history_date`` field is indexed. You can control this behavior using settings.py.
95+
96+
.. code-block:: python
97+
98+
# disable indexing on history_date
99+
SIMPLE_HISTORY_DATE_INDEX = False
100+
101+
# enable indexing on history_date (default setting)
102+
SIMPLE_HISTORY_DATE_INDEX = True
103+
104+
# enable composite indexing on history_date and model pk (to improve as_of queries)
105+
# the string is case-insensitive
106+
SIMPLE_HISTORY_DATE_INDEX = "Composite"
107+
108+
89109
Custom history table name
90110
-------------------------
91111

simple_history/models.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from django.conf import settings
88
from django.contrib import admin
99
from django.contrib.auth import get_user_model
10-
from django.core.exceptions import ObjectDoesNotExist
10+
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
1111
from django.db import models
1212
from django.db.models import ManyToManyField
1313
from django.db.models.fields.proxy import OrderWrt
@@ -426,7 +426,7 @@ def get_default_history_user(instance):
426426

427427
extra_fields = {
428428
"history_id": self._get_history_id_field(),
429-
"history_date": models.DateTimeField(),
429+
"history_date": models.DateTimeField(db_index=self._date_indexing is True),
430430
"history_change_reason": self._get_history_change_reason_field(),
431431
"history_type": models.CharField(
432432
max_length=1,
@@ -451,6 +451,23 @@ def get_default_history_user(instance):
451451

452452
return extra_fields
453453

454+
@property
455+
def _date_indexing(self):
456+
"""False, True, or 'composite'; default is True"""
457+
result = getattr(settings, "SIMPLE_HISTORY_DATE_INDEX", True)
458+
valid = True
459+
if isinstance(result, str):
460+
result = result.lower()
461+
if result not in ("composite",):
462+
valid = False
463+
elif not isinstance(result, bool):
464+
valid = False
465+
if not valid:
466+
raise ImproperlyConfigured(
467+
"SIMPLE_HISTORY_DATE_INDEX must be one of (False, True, 'Composite')"
468+
)
469+
return result
470+
454471
def get_meta_options(self, model):
455472
"""
456473
Returns a dictionary of fields that will be added to
@@ -467,6 +484,8 @@ def get_meta_options(self, model):
467484
meta_fields["verbose_name"] = name
468485
if self.app:
469486
meta_fields["app_label"] = self.app
487+
if self._date_indexing == "composite":
488+
meta_fields["index_together"] = (("history_date", model._meta.pk.attname),)
470489
return meta_fields
471490

472491
def post_save(self, instance, created, using=None, **kwargs):
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Generated by Django 4.0.dev20210811195242 on 2021-08-13 10:07
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
(
10+
"migration_test_app",
11+
"0003_alter_historicalmodelwithcustomattrforeignkey_options_and_more",
12+
),
13+
]
14+
15+
operations = [
16+
migrations.AlterField(
17+
model_name="historicalmodelwithcustomattrforeignkey",
18+
name="history_date",
19+
field=models.DateTimeField(db_index=True),
20+
),
21+
migrations.AlterField(
22+
model_name="historicalyar",
23+
name="history_date",
24+
field=models.DateTimeField(db_index=True),
25+
),
26+
]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from django.conf import settings
2+
from django.db import models
3+
from django.test import TestCase, override_settings
4+
5+
from simple_history.models import HistoricalRecords
6+
7+
8+
@override_settings(SIMPLE_HISTORY_DATE_INDEX="Composite")
9+
class HistoricalIndexTest(TestCase):
10+
def test_has_composite_index(self):
11+
self.assertEqual(settings.SIMPLE_HISTORY_DATE_INDEX, "Composite")
12+
13+
class Foo(models.Model):
14+
history = HistoricalRecords()
15+
16+
self.assertEqual(
17+
("history_date", "id"), Foo.history.model._meta.index_together[0]
18+
)

simple_history/tests/tests/test_models.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55

66
import django
77
from django.apps import apps
8+
from django.conf import settings
89
from django.contrib.auth import get_user_model
9-
from django.core.exceptions import ObjectDoesNotExist
10+
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
1011
from django.core.files.base import ContentFile
1112
from django.db import IntegrityError, models
1213
from django.db.models.fields.proxy import OrderWrt
@@ -1038,6 +1039,25 @@ def test_most_recent_nonexistant(self):
10381039
poll.delete()
10391040
self.assertRaises(Poll.DoesNotExist, poll.history.most_recent)
10401041

1042+
def test_date_indexing_options(self):
1043+
records = HistoricalRecords()
1044+
delattr(settings, "SIMPLE_HISTORY_DATE_INDEX")
1045+
self.assertTrue(records._date_indexing)
1046+
settings.SIMPLE_HISTORY_DATE_INDEX = False
1047+
self.assertFalse(records._date_indexing)
1048+
settings.SIMPLE_HISTORY_DATE_INDEX = "Composite"
1049+
self.assertEqual(records._date_indexing, "composite")
1050+
settings.SIMPLE_HISTORY_DATE_INDEX = "foo"
1051+
with self.assertRaises(ImproperlyConfigured):
1052+
records._date_indexing
1053+
settings.SIMPLE_HISTORY_DATE_INDEX = 42
1054+
with self.assertRaises(ImproperlyConfigured):
1055+
records._date_indexing
1056+
settings.SIMPLE_HISTORY_DATE_INDEX = None
1057+
with self.assertRaises(ImproperlyConfigured):
1058+
records._date_indexing
1059+
delattr(settings, "SIMPLE_HISTORY_DATE_INDEX")
1060+
10411061
def test_as_of(self):
10421062
poll = Poll.objects.create(question="what's up?", pub_date=today)
10431063
poll.question = "how's it going?"

0 commit comments

Comments
 (0)