Set operations allow you to combine results from multiple queries. All operations work across MySQL 8.0+, PostgreSQL, and SQLite 3.8.3+.
Set operations combine rows from two or more SELECT queries:
- UNION - Combines results and removes duplicates
- UNION ALL - Combines results keeping all rows (faster)
- INTERSECT - Returns rows that appear in both queries
- EXCEPT - Returns rows from first query not in second
$results = $db->find()
->from('products_eu')
->select(['name', 'price'])
->union(function ($qb) {
$qb->from('products_us')->select(['name', 'price']);
})
->get();$results = $db->find()
->from('orders_2023')
->select(['product_id', 'amount'])
->unionAll(function ($qb) {
$qb->from('orders_2024')->select(['product_id', 'amount']);
})
->get();$common = $db->find()
->from('active_users')
->select(['email'])
->intersect(function ($qb) {
$qb->from('premium_users')->select(['email']);
})
->get();$onlyInFirst = $db->find()
->from('all_products')
->select(['product_id'])
->except(function ($qb) {
$qb->from('discontinued_products')->select(['product_id']);
})
->get();You can chain multiple set operations:
$results = $db->find()
->from('sales_q1')
->select(['product_id', 'quarter' => 'Q1'])
->unionAll(function ($qb) {
$qb->from('sales_q2')->select(['product_id', 'quarter' => 'Q2']);
})
->unionAll(function ($qb) {
$qb->from('sales_q3')->select(['product_id', 'quarter' => 'Q3']);
})
->unionAll(function ($qb) {
$qb->from('sales_q4')->select(['product_id', 'quarter' => 'Q4']);
})
->orderBy('quarter')
->orderBy('product_id')
->get();$results = $db->find()
->from('products_eu')
->select(['name', 'price'])
->where('price', 100, '>')
->union(function ($qb) {
$qb->from('products_us')
->select(['name', 'price'])
->where('price', 100, '>');
})
->orderBy('price', 'DESC')
->get();$results = $db->find()
->from('sales_2023')
->select([
'year' => '2023',
'total' => Db::sum('amount'),
])
->unionAll(function ($qb) {
$qb->from('sales_2024')->select([
'year' => '2024',
'total' => Db::sum('amount'),
]);
})
->orderBy('year')
->get();These clauses apply to the final combined result:
$results = $db->find()
->from('products_eu')
->select(['name', 'price'])
->union(function ($qb) {
$qb->from('products_us')->select(['name', 'price']);
})
->orderBy('price', 'DESC') // Applies to combined result
->limit(10) // Top 10 from combined result
->get();Instead of closures, you can pass QueryBuilder instances:
$query1 = $db->find()->from('table1')->select(['col1', 'col2']);
$query2 = $db->find()->from('table2')->select(['col1', 'col2']);
$results = $query1->union($query2)->get();- All queries must return the same number of columns
- Column types should be compatible across queries
- Column names from the first query are used in the result
UNIONis slower thanUNION ALL(deduplication overhead)- Use
UNION ALLwhen you know there are no duplicates - Add indexes on columns used in WHERE clauses
- MySQL 8.0+: Full support (UNION, UNION ALL, INTERSECT, EXCEPT)
- PostgreSQL: Full support (all operations)
- SQLite 3.8.3+: Full support (all operations)
-
Use appropriate operation:
UNIONwhen duplicates matterUNION ALLfor better performanceINTERSECTfor common elementsEXCEPTfor differences
-
Column alignment:
// ✅ Good - aligned columns
$qb->from('users')->select(['id', 'name', 'email'])
->union(fn($q) => $q->from('admins')->select(['id', 'name', 'email']));
// ❌ Bad - mismatched columns
$qb->from('users')->select(['id', 'name'])
->union(fn($q) => $q->from('admins')->select(['id', 'name', 'role']));- Apply filters before UNION:
// ✅ Good - filter before union
$qb->from('products')->where('active', 1)->select(['name'])
->union(fn($q) => $q->from('services')->where('active', 1)->select(['name']));
// Less efficient - filtering combined result
$qb->from('products')->select(['name', 'active'])
->union(fn($q) => $q->from('services')->select(['name', 'active']))
->where('active', 1);See Set Operations examples for complete working examples.
- CTEs - Complex queries with WITH clauses
- Subqueries - Nested queries
- Aggregations - GROUP BY and aggregate functions