# PostGIS Indexes - refs ticket #30267

**URL:** https://forum.djangoproject.com/t/postgis-indexes-refs-ticket-30267/45702
**Category:** GeoDjango
**Created:** [August 9, 2026, 8:42am UTC](https://forum.djangoproject.com/t/postgis-indexes-refs-ticket-30267/45702 "2026-08-09T08:42:39Z")
**Posts on this page:** 1
**Page:** 1

<div class="post-metadata">

### Author: ![smithdc1](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/smithdc1/32/23160_2.png) [@smithdc1](https://forum.djangoproject.com/u/smithdc1)
#### Post date: [August 9, 2026, 8:42am UTC](https://forum.djangoproject.com/t/postgis-indexes-refs-ticket-30267/45702/1 "2026-08-09T08:42:39Z")

</div>

While reviewing a few PostGIS tickets recently, I noticed that spatial indexes are not always used. We have an accepted ticket for this [#30267 (GeoDjango does not create correct PostGIS index for 3D geometry fields) – Django](https://code.djangoproject.com/ticket/30267) with guidance to seek further consensus. The following aims to set step though the issue in an aim to gain that consensus.

By default, GIS models have an index created for geometry fields, see [docs](https://docs.djangoproject.com/en/dev/ref/contrib/gis/model-api/#django.contrib.gis.db.models.BaseSpatialField.spatial_index).

Looking specifically at Geometry fields on PostGIS, different indexes are created for 2d and 3d fields, see [source](https://github.com/django/django/blob/fb1137658ff597ffac758c3246a9bf8f0e90c753/django/contrib/gis/db/backends/postgis/schema.py#L109-L111). To bring this to life a `gist` index is created in both cases with the 3d geometry using the `gist_geometry_ops_nd` opclasses.

- For a 2d geometry the index `CREATE INDEX` looks something like this:
  - CREATE INDEX app\_building2d\_geom\_73320ff1\_id ON public.app\_building2d USING gist (geom)

- And for 3d
  - CREATE INDEX app\_building3d\_geom\_881d8105\_id ON public.app\_building3d USING gist (geom gist\_geometry\_ops\_nd)

I think using ``gist_geometry_ops_nd` ` for 3d fields is a poor default choice and we should remove that opclass.

Example time. Here’s my workings. Let’s step through them. [DryORM - Django ORM Playground](https://dryorm.xterm.info/postgis-indexes)

9 square polygons are created, and we can filter for those that intersect with the blue outline. The query for this is:

`Building2D.objects.filter(geom__intersects=bbox)`

 ![image](https://us1.discourse-cdn.com/flex026/uploads/djangoproject/original/3X/e/d/edf9d0a7987f92102a0b2037e606a18528024f02.png)

In both cases the query returns 4 geomeries; however an `explain` shows that in the 2D model the index was used [See the ‘Explain Output’ section for `(Building2D.intersects(bbox))`] but on the 3D model a sequential scan was used [`(Building3D.intersects(bbox))`]

Looking at the PostGIS [docs](https://postgis.net/workshops/postgis-intro/indexing.html#spatially-indexed-functions) on the topic only a:

> subset of functions make automatic use of a spatial index

And of this subset there are 3 which use a 3D index - none of which Django currently has support for.

- ST\_3DIntersects
- ST\_3DDWithin
- ST\_3DDFullyWithin

Looking at the docs on [gist\_geometry\_ops\_nd](https://postgis.net/workshops/postgis-intro/3d.html#n-d-indexes) we can also see that it can be used in the ``&&&`` index operator. Again Django doesn’t have support for this. See [source](https://github.com/django/django/blob/fb1137658ff597ffac758c3246a9bf8f0e90c753/django/contrib/gis/db/backends/postgis/operations.py#L136-L168).

As far as I can tell it is not possible to write a query using the provided functions that make use of the default index created on 3D geometries.

PostGIS docs are also quite cautious about recommending a `nd` index, [see docs](https://postgis.net/workshops/postgis-intro/3d.html#n-d-indexes).

> For a higher dimension index to be useful, the data must cover a wide range of that dimension, relative to the kinds of queries you are constructing.
> 
> - A set of DEM points would probably be a _poor_ candidate for a 3-D index, since the queries would usually be extracting a 2-D box of points, and rarely attempting to select a Z-slice of points.
> 
> - A set of GPS traces in X/Y/T space might be a _good_ candidate for a 3-D index, if the GPS tracks overlapped each other frequently in all dimensions (for example, driving the same route over and over at different times), since there would be large variability in all dimensions of the data set.

The rest of the script goes on to show that you can add the `ST_3DIntersects` as a lookup and that this will result in an Index Scan for both 2d and 3d models. However, the behaviour is different – if there’s a 3rd dimension in the supplied geometry this will play a part in the query result.

Finally, it also shows index being used for 2d within and contains lookups but not on the 3d model.

**What to do?**

I think the `gist_geometry_ops_nd` opclass should be removed when creating 3d indexes and as this is a better default. If folk want to retain that index they can add it using `Meta.Indexes`. Even if we were to add 3d lookups they don’t seem to be as useful as the 2d ones.

If we were to change the default I am unsure how we could go about this. Simply changing the type of index would be backward incompatable change and would leave databases in a different state than if migrations were applied from scratch. Just documenting the change seems not ideal to me.

I thought about an alternantive (lots of hand waving incoming) where we add a new kwarg to `GeometryField`/`BaseSpatialField`. Let’s call it “index\_3d” (working title) and defaults to `True`. We could then immediately deprecate that to nudge folk to either set it to `False`, or they can set `spatial_index` to False and add a 3D index via `Meta.Indexes` (although that would probably add an add/remove index actions, not sure if that could be combined into a rename?). I’m not sure how plausible this approach is.

If we were to agree a change in the default is the right decision we can likely explore the implementation options further as a next step.

cc / @jacobtylerwalls
