The Django ORM lets developers interact with the database using Python code rather than raw SQL. The default manager on every model is objects, and the all() method returns a QuerySet containing all rows in the table. To narrow results, filter() returns a QuerySet of zero or more objects, while get() returns a single object or raises DoesNotExist or MultipleObjectsReturned exceptions. The exclude() method is the inverse of filter(), returning objects that do not match the given criteria. Field lookups support powerful queries using double-underscore syntax, such as title__icontains='django', date__gte=some_date, and author__name='Alice', enabling complex searches without writing SQL directly.
Several QuerySet methods help optimize database access. The count() method issues a SELECT COUNT(*) query, which is far cheaper than using len(queryset), which forces evaluation of the full queryset. The exists() method is even more efficient when only the presence of records is needed, as it issues a fast SELECT 1 ... LIMIT 1 query. The values() method returns dictionaries of field values, while values_list() returns tuples; with flat=True and a single field, values_list() returns a flat list. The only() and defer() methods control which fields are loaded from the database, useful for avoiding unnecessary I/O on large TextField columns.
Django provides several expression types for advanced queries. The Q object enables complex lookups with OR, AND, and NOT operators, since multiple filter() arguments are combined with AND by default. The F expression references a model field value directly in the database, avoiding a Python round-trip, which is critical for atomic operations like Article.objects.update(views=F('views') + 1). The aggregate() method returns a dictionary of aggregated values using functions like Count, Avg, and Sum, while annotate() adds aggregated fields to each object in the queryset. Subqueries can be embedded using Subquery() and Exists() from django.db.models, often paired with OuterRef() to reference the outer query. The N+1 query problem, which occurs when accessing related objects in a loop triggers one extra query per parent row, is typically solved using select_related() for ForeignKey and OneToOneField relationships, and prefetch_related() for ManyToManyField and reverse relations. For data integrity, transaction.atomic groups multiple SQL operations so they commit or roll back atomically, while select_for_update() locks selected rows for the duration of a transaction to prevent race conditions. Bulk operations like bulk_create(), queryset update(), and queryset delete() allow efficient batch modifications.