Skip to main content

PostgreSQL and SQLite partial indexes for Django models

Project description

django-partial-index

Build Status PyPI version

Partial (sometimes also called filtered or conditional) index support for Django.

With partial indexes, only some subset of the rows in the table have corresponding index entries. This can be useful for optimizing index size and query speed, and to add unique constraints for only selected rows.

More info on partial indexes:

Partial indexes now included in Django

Since the release of Django 2.2 LTS in April 2019, partial indexes are now supported by standard Django.

These are called index conditions there.

The django-partial-index package will live on in maintenance mode.

It can be useful if you are maintaining a project on and older version of Django, or wish to migrate django-partial-index indexes to Django 2.2 style on your own schedule.

Install

pip install django-partial-index

Requirements:

  • Django 1.11, 2.0, 2.1 or 2.2,
  • Python 2.7, 3.4, 3.5, 3.6 or 3.7 (as supported by the Django version),
  • PostgreSQL or SQLite database backend. (Partial indexes are not supported on MySQL, and require major hackery on Oracle.)

All Python versions which Django supports are also supported by this package. These are:

  • Django 1.11 - Python 2.7 and 3.4 - 3.7,
  • Django 2.0 - Python 3.4 - 3.7,
  • Django 2.1 - Python 3.5 - 3.7,
  • Django 2.2 - Python 3.5 - 3.7.

Usage

Set up a PartialIndex and insert it into your model's class-based Meta.indexes list:

from partial_index import PartialIndex, PQ

class MyModel(models.Model):
    class Meta:
        indexes = [
            PartialIndex(fields=['user', 'room'], unique=True, where=PQ(deleted_at__isnull=True)),
            PartialIndex(fields=['created_at'], unique=False, where=PQ(is_complete=False)),
        ]

The PQ uses the exact same syntax and supports all the same features as Django's Q objects (see Django docs for a full tutorial). It is provided for compatibility with Django 1.11.

Of course, these (unique) indexes could be created by a handwritten RunSQL migration. But the constraints are part of the business logic, and best kept close to the model definitions.

Partial unique constraints

With unique=True, this can be used to create unique constraints for a subset of the rows.

For example, you might have a model that has a deleted_at field to mark rows as archived instead of deleting them forever. You wish to add unique constraints on "alive" rows, but allow multiple copies in the archive. Django's unique_together is not sufficient here, as that cannot distinguish between the archived and alive rows.

from partial_index import PartialIndex, PQ

class RoomBooking(models.Model):
    user = models.ForeignKey(User)
    room = models.ForeignKey(Room)
    deleted_at = models.DateTimeField(null=True, blank=True)

    class Meta:
        # unique_together = [('user', 'room')] - Does not allow multiple deleted rows. Instead use:
        indexes = [
            PartialIndex(fields=['user', 'room'], unique=True, where=PQ(deleted_at__isnull=True))
        ]

Partial non-unique indexes

With unique=False, partial indexes can be used to optimise lookups that return only a small subset of the rows.

For example, you might have a job queue table which keeps an archive of millions of completed jobs. Among these are a few pending jobs, which you want to find with a .filter(is_complete=0) query.

from partial_index import PartialIndex, PQ

class Job(models.Model):
    created_at = models.DateTimeField(auto_now_add=True)
    is_complete = models.IntegerField(default=0)

    class Meta:
        indexes = [
            PartialIndex(fields=['created_at'], unique=False, where=PQ(is_complete=0))
        ]

Compared to an usual full index on the is_complete field, this can be significantly smaller in disk and memory use, and faster to update.

Referencing multiple fields in the condition

With F-expressions, you can create conditions that reference multiple fields:

from partial_index import PartialIndex, PQ, PF

class NotTheSameAgain(models.Model):
    a = models.IntegerField()
    b = models.IntegerField()

    class Meta:
        indexes = [
            PartialIndex(fields=['a', 'b'], unique=True, where=PQ(a=PF('b'))),
        ]

This PartialIndex allows multiple copies of (2, 3), but only a single copy of (2, 2) to exist in the database.

The PF uses the exact same syntax and supports all the same features as Django's F expressions (see Django docs for a full tutorial). It is provided for compatibility with Django 1.11.

Unique validation on ModelForms

Unique partial indexes are validated by the PostgreSQL and SQLite databases. When they reject an INSERT or UPDATE, Django raises a IntegrityError exception. This results in a 500 Server Error status page in the browser if not handled before the database query is run.

ModelForms perform unique validation before saving an object, and present the user with a descriptive error message.

Adding an index does not modify the parent model's unique validation, so partial index validations are not handled by them by default. To add that to your model, include the ValidatePartialUniqueMixin in your model definition:

from partial_index import PartialIndex, PQ, ValidatePartialUniqueMixin

class MyModel(ValidatePartialUniqueMixin, models.Model):
    class Meta:
        indexes = [
            PartialIndex(fields=['user', 'room'], unique=True, where=PQ(deleted_at__isnull=True)),
        ]

Note that it should be added on the model itself, not the ModelForm class.

Adding the mixin for non-unique partial indexes is unnecessary, as they cannot cause database IntegrityErrors.

Text-based where-conditions (deprecated)

Text-based where-conditions are deprecated and will be removed in the next release (0.6.0) of django-partial-index.

They are still supported in version 0.5.0 to simplify upgrading existing projects to the PQ-based indexes. New projects should not use them.

from partial_index import PartialIndex

class TextExample(models.Model):
    class Meta:
        indexes = [
            PartialIndex(fields=['user', 'room'], unique=True, where='deleted_at IS NULL'),
            PartialIndex(fields=['created_at'], unique=False, where_postgresql='is_complete = false', where_sqlite='is_complete = 0')
        ]

Version History

0.6.0 (latest)

  • Add support for Django 2.2.
  • Document (already existing) support for Django 2.1 and Python 3.7.

0.5.2

  • Fix makemigrations for Django 1.11.
  • Make sure PQ and PF are imported directly from partial_index in migration files.

0.5.1

  • Fix README formatting in PyPI.

0.5.0

  • Add support for Q-object based where-expressions.
  • Deprecate support for text-based where-expressions. These will be removed in version 0.6.0.
  • Add ValidatePartialUniqueMixin for model classes. This adds partial unique index validation for ModelForms, avoiding an IntegrityError and instead showing an error message as with usual unique_together constraints.

0.4.0

  • Add support for Django 2.0.

0.3.0

  • Add support for separate where_postgresql='' and where_sqlite='' predicates, when the expression has different syntax on the two database backends and you wish to support both.

0.2.1

  • Ensure that automatically generated index names depend on the "unique" and "where" parameters. Otherwise two indexes with the same fields would be considered identical by Django.

0.2.0

  • Fully tested SQLite and PostgreSQL support.
  • Tests for generated SQL statements, adding and removing indexes, and that unique constraints work when inserting rows into the db tables.
  • Python 2.7, 3.4-3.6 support.

0.1.1

  • Experimental SQLite support.

0.1.0

  • First release, working but untested PostgreSQL support.

Future plans

  • Add a validation mixin for DRF Serializers.
  • Remove support for text-based where conditions.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django-partial-index-0.6.0.tar.gz (10.0 kB view details)

Uploaded Source

Built Distribution

django_partial_index-0.6.0-py2.py3-none-any.whl (12.0 kB view details)

Uploaded Python 2 Python 3

File details

Details for the file django-partial-index-0.6.0.tar.gz.

File metadata

  • Download URL: django-partial-index-0.6.0.tar.gz
  • Upload date:
  • Size: 10.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/1.13.0 pkginfo/1.5.0.1 requests/2.22.0 setuptools/41.0.1 requests-toolbelt/0.9.1 tqdm/4.32.2 CPython/3.7.0

File hashes

Hashes for django-partial-index-0.6.0.tar.gz
Algorithm Hash digest
SHA256 c3c3b31c4fc73f772edd5ad3b8421cb410b5a5d681b45fc6d6956587952a5edb
MD5 74850829055428b0b5e83712a3968e4b
BLAKE2b-256 ca0cdebd3f67abb4508e449e938c1e172dec1aac4ff7fff9d15048a32e8d41a0

See more details on using hashes here.

File details

Details for the file django_partial_index-0.6.0-py2.py3-none-any.whl.

File metadata

  • Download URL: django_partial_index-0.6.0-py2.py3-none-any.whl
  • Upload date:
  • Size: 12.0 kB
  • Tags: Python 2, Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/1.13.0 pkginfo/1.5.0.1 requests/2.22.0 setuptools/41.0.1 requests-toolbelt/0.9.1 tqdm/4.32.2 CPython/3.7.0

File hashes

Hashes for django_partial_index-0.6.0-py2.py3-none-any.whl
Algorithm Hash digest
SHA256 77dde49b20560fc489ec7270484926fc175795d852344df9421f7feec283f783
MD5 00d39e24257c15d16277f5442cef6a73
BLAKE2b-256 69845ffd927bc2805d62386804bf107d6ddef0fe75f305087347fb0a717874eb

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page