Combining QuerySets¶
Edgy's QuerySet API supports SQL-style set operations such as UNION, UNION ALL, INTERSECT, and EXCEPT.
Overview¶
| Operation | Description | SQL Equivalent |
|---|---|---|
.union(qs2) |
Combines both querysets, removing duplicates. | UNION |
.union_all(qs2) |
Combines both querysets, keeping duplicates. | UNION ALL |
.intersect(qs2) |
Returns only rows appearing in both querysets. | INTERSECT |
.except_(qs2) |
Returns rows from the first queryset that aren't in the second. | EXCEPT |
All methods return a CombinedQuerySet, which behaves just like a normal queryset.
You can continue chaining methods such as:
Example Models¶
import edgy
class Customer(edgy.StrictModel):
name = edgy.CharField(max_length=100)
country = edgy.CharField(max_length=2)
class Meta:
registry = models
class Order(edgy.StrictModel):
customer = edgy.ForeignKey(Customer, related_name="orders")
total = edgy.DecimalField(max_digits=10, decimal_places=2)
status = edgy.CharField(max_length=20)
class Meta:
registry = models
Union — Combine QuerySets¶
Combine multiple querysets of the same model.
# Customers from Switzerland and Germany
swiss = Customer.query.filter(country="CH")
german = Customer.query.filter(country="DE")
# Combine them into a single queryset
european = swiss.union(german).order_by("name")
customers = await european.all()
Generated SQL:
SELECT * FROM customer WHERE country = 'CH'
UNION
SELECT * FROM customer WHERE country = 'DE'
ORDER BY name;
This deduplicates automatically, preserving unique customers.
Union All — Keep Duplicates¶
recent_orders = Order.query.filter(status="recent")
priority_orders = Order.query.filter(status="priority")
combined_orders = recent_orders.union_all(priority_orders)
orders = await combined_orders.order_by("-total").limit(10)
This returns all matching rows, even duplicates.
Intersect — Shared Records Between Sets¶
active_customers = Customer.query.filter(status="active")
premium_customers = Customer.query.filter(plan="premium")
shared = active_customers.intersect(premium_customers)
customers = await shared.order_by("name")
Returns only customers that satisfy both filters.
Except — Subtract One Set from Another¶
all_customers = Customer.query.all()
has_orders = Customer.query.filter(orders__isnull=False)
new_customers = all_customers.except_(has_orders)
await new_customers.count()
Returns only customers without any orders.
Chaining and Nesting¶
You can freely chain set operations — Edgy handles them as combined SQL subqueries.
# (CH ∪ DE) ∪ FR
union_3 = (
Customer.query.filter(country="CH")
.union(Customer.query.filter(country="DE"))
.union(Customer.query.filter(country="FR"))
)
customers = await union_3.order_by("name")
You can also mix operations:
# (CH ∪ DE) - FR
qs = (
Customer.query.filter(country="CH")
.union(Customer.query.filter(country="DE"))
.except_(Customer.query.filter(country="FR"))
)
Behavior Details¶
Ordering, Limit, and Offset¶
Outer modifiers apply to the entire combined result:
Deferred Fields and only()¶
These propagate safely across combined querysets:
qs1 = Customer.query.only("id", "name")
qs2 = Customer.query.defer("country")
combined = qs1.union(qs2).order_by("name")
rows = await combined.values(["id", "name"])
Counting and Existence¶
Model and Registry Safety¶
Both sides of the operation must come from the same model and registry.
from edgy.exceptions import QuerySetError
with pytest.raises(QuerySetError):
await Customer.query.union(Order.query)
Complex Example — Filtering for Reporting¶
eu_customers = (
Customer.query.filter(country="DE")
.union(Customer.query.filter(country="CH"))
.except_(Customer.query.filter(orders__isnull=False))
.order_by("name")
.limit(20)
)
for c in await eu_customers:
print(c.name, c.country)
Find customers in the EU who are German or Swiss but haven't ordered yet.
Tip
Always include .order_by() when comparing lists to ensure deterministic results.
Summary¶
| Feature | Supported |
|---|---|
union() |
Deduplicates results |
union_all() |
Keeps duplicates |
intersect() |
Common records |
except_() |
Subtracts records |
| Cross-model operations | ❌ Raises QuerySetError |