recreating IS NOT DISTINCT FROM in django ORM

Hello everyone, I’m trying to recreate this sql using Django ORM, how would I go about it?

SELECT food.*
FROM food
         LEFT JOIN sweet
                   ON (sweet.fruit_id = food.fruit_id) AND
                      (sweet.category_id IS NOT DISTINCT FROM food.category_id);

so far I found out I can do

Food.objects.annotate(sweet_id=fruit__sweets__id)

to recreate

SELECT food.*
FROM food
         LEFT JOIN sweet
                   ON (sweet.fruit_id = food.fruit_id)

But I have a hard time recreating the

AND (sweet.category_id IS NOT DISTINCT FROM food.category_id);

on it’s own or even combined

Welcome @Malvi1697 !

Django itself does not directly support the ‘is distinct from’ SQL operator.

You’ve got a couple of options for handling this yourself.

Side note:

This may not be necessary depending upon the full context of what you’re trying to do here with this data after it has been queried. Django handles a lot of these joins implicitly - there’s rarely a need to try and “force” a join to occur.

You need to look at this as a whole - not just the immediate query you’re trying to execute, but what you’re going to do with the results of that query within the view.

2 Likes

Thank you, Ken. I’ve continued investigating and developed this query. While it doesn’t exactly correspond to these SQL joins, the results are nearly identical, with only five rows missing out of 90,000. I’ve done this:​

Food.objects.select_related('fruit').prefetch_related(
    'fruit__sweets'
).filter(
    Q(
        fruit__sweets__category=F('category')
    ) |
    Q(
        fruit__sweets__category__isnull=True,
        category__isnull=True
    ) |
    Q(fruit__sweets__isnull=True)
)
)

Again, what are you doing with the results of this query?

Depending upon usage, you may not need these:

These clauses are not required for generating references to related data within the query. They are only a performance enhancement when accessing this related data from the objects that have been retrieved.

2 Likes

While it doesn’t exactly correspond to these SQL joins, the results are nearly identical

If you want to achieve the exact same SQL you should write a custom lookup as @KenWhitesell pointed out

from django.db.models import Field, Lookup

@Field.register_lookup
class DistinctFrom(Lookup):
   lookup_name = "distinctfrom"

   def as_sql(self, compiler, connection):
        lhs, lhs_params = self.process_lhs(compiler, connection)
        rhs, rhs_params = self.process_rhs(compiler, connection)
        params = lhs_params + rhs_params
        return "%s IS DISTINCT FROM %s" % (lhs, rhs), params

and use a FilteredRelation annotation

Fruit.objects.annotate(
   filtered_fruits_sweets=FilteredRelation(
       "fruits__sweets",
       condition=~Q(fruits__sweets__category__distinctfrom=F("category"))
   )
)

Note that you’ll likely have to explicitly reference filtered_fruits_sweets for the filtering to be applied as the Django ORM is not a query builder per-se but a way to retrieve data.

With that in mind it’s much better to approach these kind problem in terms of what exactly you are trying to do with the data, and communicate it in your requests for support, than the other way around as Ken pointed out.

1 Like

Thanks for the great solution and example! However, I ran into a strange issue when using only FilteredRelation. Here’s my code:

# this changes dynamically
event_type_id = "60ae604c-90e1-495b-8a0e-512f29676e39"
result = (
    Plan.objects.annotate(
        filtered_eventplan=FilteredRelation(
            "part__event_plans",
            condition=Q(
                part__event_plans__event_type_id=event_type_id,
                part__event_plans__part_id=F("part_id"),
            )
            & Q(
                Q(part__event_plans__circuit_id=F("circuit_id"))
                | (
                    Q(part__event_plans__circuit_id__isnull=True)
                    & Q(circuit_id__isnull=True)
                )
            ),
        )
    )
    .filter(
        Q(foreign=False)
        & (Q(cancellation_date__isnull=True) | Q(cancellation_date__gt=now.date()))
    )
    .annotate(identifier=F("filtered_eventplan__event_type__identifier"))
)

Whenever I try to annotate a field that references a foreign key used in the FilteredRelation condition, I get a ProgrammingError:

django.db.utils.ProgrammingError: missing FROM-clause entry for table "filtered_eventplan"
LINE 1: ...") LEFT OUTER JOIN "administration_eventtype" ON (filtered_e...

For example, since I use part__event_plans__part and part__event_plans__event_type (both foreign keys) in the FilteredRelation I cannot access them in my next .annotate(). The raw SQL join generated looks likes this:

         LEFT OUTER JOIN "department_part" ON ("department_plan"."part_id" = "department_part"."id")
         LEFT OUTER JOIN "administration_eventtype"
                         ON (filtered_eventplan."event_type_id" = "administration_eventtype"."id")
         LEFT OUTER JOIN "department_partcircuit"
                         ON ("department_plan"."circuit_id" = "department_partcircuit"."id")
         LEFT OUTER JOIN "department_eventplan" filtered_eventplan
                         ON ("department_part"."id" = filtered_eventplan."part_id" AND
                             ((filtered_eventplan."event_type_id" =
                               '60ae604c-90e1-495b-8a0e-512f29676e39' AND
                               filtered_eventplan."part_id" = ("department_plan"."part_id") AND
                               (filtered_eventplan."circuit_id" =
                                ("department_plan"."circuit_id") OR
                                (filtered_eventplan."circuit_id" IS NULL AND
                                 "department_plan"."circuit_id" IS NULL)))))
WHERE (NOT "department_plan"."foreign" AND ("department_plan"."cancellation_date" IS NULL OR
                                            "department_plan"."cancellation_date" >
                                            '2025-03-05 00:00:00+01:00'))

In the SQL output, you can see this join:

         LEFT OUTER JOIN "administration_eventtype"
                         ON (filtered_eventplan."event_type_id" = "administration_eventtype"."id")
         LEFT OUTER JOIN "department_partcircuit"
                         ON ("department_plan"."circuit_id" = "department_partcircuit"."id")
         LEFT OUTER JOIN "department_eventplan" filtered_eventplan
                         ON ("department_part"."id" = filtered_eventplan."part_id" AND

This means Django is trying to reference filtered_eventplan before it exists in the query, causing the ProgrammingError

I’m a junior developer and have been programming for six months. This is my first project, so sorry if I overexplained or underexplained anything—I’m still learning how to structure technical questions effectively.

I am not sure if you’ll get a ping since I didn’t hit reply, therefore just to be sure I’m replying here.

It’s possible that you are either running into a FilteredRelation bug or running an older version of Django.

Could you provide the version of Django your are using and your model definition?

Django version: 5.0.11

class EventPlan(ProjectModel):
    objects = EventPlanManager()

    part = models.ForeignKey(
        "department.Part", on_delete=models.CASCADE, related_name="event_plans"
    )
    circuit = models.ForeignKey(
        "department.PartCircuit",
        models.DO_NOTHING,
        related_name="event_plans",
        blank=True,
        null=True,
    )
    event_type = models.ForeignKey(
        "administration.EventType",
        models.DO_NOTHING,
        related_name="event_plans",
        null=True,
        blank=True,
    )
...
class Plan(ProjectModel):
    objects = PlanManager()

    device = models.ForeignKey(
        "Device", on_delete=models.CASCADE, related_name="plans", blank=True, null=True
    )
    part = models.ForeignKey(
        "Part", on_delete=models.CASCADE, related_name="plans", blank=True, null=True
    )
    circuit = models.ForeignKey(
        "PartCircuit", models.DO_NOTHING, related_name="plans", blank=True, null=True
    )
...

The first thing I’d try is upgrading to Django 5.1 (or even 5.2b1) as it contains a few FilteredRelation bugfixes. It that doesn’t cut it there might another bug lurking here that needs further diagnosis.

Okay, I will try it tomorrow and report back soon. Thank you!