Why do queryset[0] and queryset.first() return different records?

SaintWacko :

I discovered today that I can access elements in a queryset by referencing them with an index, i.e. queryset[n]. However, immediately after I discovered that queryset[0] does not return the same record as queryset.first(). Why is this, and is one of those more "correct"? (I know that .first() is faster, but other than that)

Python 3.7.4
django 1.11.20

Willem Van Onsem :

There is a small semantical difference between qs[0] and qs.first(). If you did not specify an order in the queryset yourself, then Django will order the queryset itself by primary key before fetching the first element.

Furthermore .first() will return None if the queryset is empty. Whereas qs[0] will raise an IndexError.

The claim that .first() is faster however is not True. In fact if you use qs[n], then Django will, behind the curtains fetch the record by slicing with qs[n:n+1], hence it will, given the database backend supports this, make a query with LIMIT 1 OFFSET n, and thus fetch one record, just like .first() will do. If the queryset is already retrieved, it will furthermore make no extra queries at all, since the data is already cached.

You can see the implementation on GitHub:

    def first(self):
        """
        Returns the first object of a query, returns None if no match is found.
        """
        objects = list((self if self.ordered else self.order_by('pk'))[:1])
        if objects:
            return objects[0]
        return None

As you can see, if the queryset is already ordered (self.ordered is True, then we take self[:1], check if there is a record, and if so return it. If not we return None.

The code for retrieving an item at a specific index is more cryptic. It essentially will set the limits from k to k+1, materialize the item, and return the first item, as we can see in the source code [GitHub]:

    def __getitem__(self, k):
        """
        Retrieves an item or slice from the set of results.
        """
        if not isinstance(k, (slice,) + six.integer_types):
            raise TypeError
        assert ((not isinstance(k, slice) and (k >= 0)) or
                (isinstance(k, slice) and (k.start is None or k.start >= 0) and
                 (k.stop is None or k.stop >= 0))), \
            "Negative indexing is not supported."

        if self._result_cache is not None:
            return self._result_cache[k]

        if isinstance(k, slice):
            qs = self._clone()
            if k.start is not None:
                start = int(k.start)
            else:
                start = None
            if k.stop is not None:
                stop = int(k.stop)
            else:
                stop = None
            qs.query.set_limits(start, stop)
            return list(qs)[::k.step] if k.step else qs

        qs = self._clone()
        qs.query.set_limits(k, k + 1)
        return list(qs)[0]

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=27858&siteId=1