The FILTER clause allows conditional aggregation without subqueries, making queries more readable and efficient. It's part of the SQL:2003 standard and automatically falls back to CASE WHEN on MySQL.
Use FILTER for:
- Conditional aggregations in a single query
- Calculating multiple metrics with different conditions
- Cleaner alternative to
CASE WHENexpressions - Cross-database compatibility (automatic fallback)
use tommyknocker\pdodb\helpers\Db;
$results = $db->find()
->from('orders')
->select([
'total_orders' => Db::count('*'),
'paid_orders' => Db::count('*')->filter('status', 'paid'),
'pending_orders' => Db::count('*')->filter('status', 'pending'),
])
->get();
// Result: [
// 'total_orders' => 100,
// 'paid_orders' => 75,
// 'pending_orders' => 25
// ]$results = $db->find()
->from('orders')
->select([
'total_revenue' => Db::sum('amount'),
'paid_revenue' => Db::sum('amount')->filter('status', 'paid'),
'pending_amount' => Db::sum('amount')->filter('status', 'pending'),
])
->get();The real power of FILTER shines with GROUP BY:
$results = $db->find()
->from('orders')
->select([
'user_id',
'total_orders' => Db::count('*'),
'paid_orders' => Db::count('*')->filter('status', 'paid'),
'pending_amount' => Db::sum('amount')->filter('status', 'pending'),
'high_value_orders' => Db::count('*')->filter('amount', 100, '>'),
])
->groupBy('user_id')
->get();FILTER works with all aggregate functions:
Db::count('*')->filter('status', 'active')
Db::count('*')->filter('amount', 100, '>')Db::sum('amount')->filter('status', 'paid')
Db::sum('quantity')->filter('category', 'Electronics')Db::avg('rating')->filter('verified', 1)
Db::avg('price')->filter('category', 'Premium')Db::min('price')->filter('available', 1)
Db::max('temperature')->filter('sensor_status', 'active')// Using multiple filter() calls creates AND condition
$result = $db->find()
->from('products')
->select([
'premium_electronics' => Db::count('*')
->filter('category', 'Electronics')
->filter('price', 500, '>'),
])
->get();// Equals (default)
Db::count('*')->filter('status', 'active')
// Greater than
Db::sum('amount')->filter('amount', 100, '>')
// Less than
Db::count('*')->filter('age', 18, '<')
// Greater than or equal
Db::avg('rating')->filter('rating', 4, '>=')
// Less than or equal
Db::count('*')->filter('stock', 10, '<=')
// Not equal
Db::count('*')->filter('status', 'deleted', '!=')$dashboard = $db->find()
->from('orders')
->select([
'total_orders' => Db::count('*'),
'completed_orders' => Db::count('*')->filter('status', 'completed'),
'cancelled_orders' => Db::count('*')->filter('status', 'cancelled'),
'total_revenue' => Db::sum('amount'),
'completed_revenue' => Db::sum('amount')->filter('status', 'completed'),
'refunded_amount' => Db::sum('amount')->filter('status', 'refunded'),
'high_value_orders' => Db::count('*')->filter('amount', 200, '>'),
])
->getOne();$segments = $db->find()
->from('users')
->select([
'country',
'total_users' => Db::count('*'),
'active_users' => Db::count('*')->filter('last_login_days', 7, '<'),
'premium_users' => Db::count('*')->filter('subscription', 'premium'),
'trial_users' => Db::count('*')->filter('subscription', 'trial'),
'avg_age' => Db::avg('age'),
'avg_active_age' => Db::avg('age')->filter('last_login_days', 30, '<'),
])
->groupBy('country')
->orderBy('total_users', 'DESC')
->get();$analysis = $db->find()
->from('sales')
->select([
'region',
'total_sales' => Db::count('*'),
'q1_sales' => Db::count('*')->filter('quarter', 'Q1'),
'q2_sales' => Db::count('*')->filter('quarter', 'Q2'),
'q3_sales' => Db::count('*')->filter('quarter', 'Q3'),
'q4_sales' => Db::count('*')->filter('quarter', 'Q4'),
'total_revenue' => Db::sum('amount'),
'q1_revenue' => Db::sum('amount')->filter('quarter', 'Q1'),
'q2_revenue' => Db::sum('amount')->filter('quarter', 'Q2'),
])
->groupBy('region')
->get();- PostgreSQL: ✅ Full native support
- SQLite 3.30+: ✅ Full native support
Generated SQL:
COUNT(*) FILTER (WHERE status = 'paid')
SUM(amount) FILTER (WHERE status = 'paid')- MySQL: ✅ Automatic fallback to CASE WHEN
Generated SQL:
COUNT(CASE WHEN status = 'paid' THEN 1 END)
SUM(CASE WHEN status = 'paid' THEN amount END)The library automatically detects the database and uses the appropriate syntax.
- Single Query: All metrics in one query instead of multiple
- Efficient: Database optimizers handle FILTER well
- Readable: Clear intent vs complex CASE statements
// ✅ Good - Single query with FILTER
$db->find()->select([
'paid' => Db::count('*')->filter('status', 'paid'),
'pending' => Db::count('*')->filter('status', 'pending'),
])->getOne();
// ❌ Bad - Multiple queries
$paid = $db->find()->where('status', 'paid')->count();
$pending = $db->find()->where('status', 'pending')->count();Add indexes on filtered columns for better performance:
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_amount ON orders(amount);// ❌ Multiple queries - slower
$total = $db->find()->from('orders')->count();
$paid = $db->find()->from('orders')->where('status', 'paid')->count();
$pending = $db->find()->from('orders')->where('status', 'pending')->count();
// ✅ Single query with FILTER - faster
$stats = $db->find()
->from('orders')
->select([
'total' => Db::count('*'),
'paid' => Db::count('*')->filter('status', 'paid'),
'pending' => Db::count('*')->filter('status', 'pending'),
])
->getOne();// ❌ Manual CASE WHEN - verbose
$db->find()->select([
'paid' => Db::raw('COUNT(CASE WHEN status = ? THEN 1 END)', ['paid']),
])->get();
// ✅ FILTER - clean and cross-database
$db->find()->select([
'paid' => Db::count('*')->filter('status', 'paid'),
])->get();// ❌ Subqueries - complex and slower
$db->find()->select([
'total' => Db::count('*'),
'paid' => Db::raw('(SELECT COUNT(*) FROM orders WHERE status = ?)'),
])->get();
// ✅ FILTER - efficient and readable
$db->find()->select([
'total' => Db::count('*'),
'paid' => Db::count('*')->filter('status', 'paid'),
])->get();- Use FILTER for conditional aggregations:
// ✅ Good - clean conditional aggregation
Db::count('*')->filter('status', 'active')
// ❌ Avoid - manual CASE WHEN
Db::raw('COUNT(CASE WHEN status = ? THEN 1 END)', ['active'])- Combine with GROUP BY for segmentation:
$db->find()
->select([
'category',
'total' => Db::count('*'),
'active' => Db::count('*')->filter('status', 'active'),
])
->groupBy('category')
->get();- Use appropriate operators:
// Numeric comparisons
Db::sum('amount')->filter('amount', 100, '>')
// String equality
Db::count('*')->filter('status', 'paid')- Add indexes on filtered columns:
CREATE INDEX idx_status ON orders(status);
CREATE INDEX idx_amount ON orders(amount);- Parameter binding: Currently uses direct parameter management
- Complex conditions: For complex logic, consider WHERE or HAVING
- Cross-column filters: Filter applies to single column comparisons
See examples/02-intermediate/02-aggregations.php for complete working examples (Examples 8-9).
- Aggregations - Aggregate helper functions
- Conditional Logic - GROUP BY and HAVING
- Performance Tips - Query optimization