Fix incorrect priority ordering when using denormalized aggregate columns. Use when: (1) Records are processed in wrong order despite ORDER BY on count/sum columns, (2) Top items by some metric aren't being selected first, (3) Aggregate columns show 0 or NULL for records that should have high values, (4) Priority queue processes low-value items before high-value ones. The root cause is often that denormalized columns (vine_count, loop_count, total_orders, etc.) weren't backfilled or maintaine...
Scanned 5/27/2026
Install via CLI
openskills install divinevideo/divine-mobile---
name: denormalized-priority-column-staleness
description: |
Fix incorrect priority ordering when using denormalized aggregate columns. Use when:
(1) Records are processed in wrong order despite ORDER BY on count/sum columns,
(2) Top items by some metric aren't being selected first, (3) Aggregate columns
show 0 or NULL for records that should have high values, (4) Priority queue
processes low-value items before high-value ones. The root cause is often that
denormalized columns (vine_count, loop_count, total_orders, etc.) weren't
backfilled or maintained properly. Solution: JOIN with source tables to compute
actual aggregates at query time.
author: Claude Code
version: 1.0.0
date: 2026-01-28
---
# Denormalized Priority Column Staleness
## Problem
When ordering records by denormalized aggregate columns (like `total_loops`, `vine_count`,
`order_count`), the query returns items in the wrong priority order because the
denormalized values are stale, unpopulated, or incorrect.
## Context / Trigger Conditions
- A batch job processes items in unexpected order
- Top items (by some aggregate metric) are processed last or skipped
- `ORDER BY aggregate_column DESC` doesn't return expected results
- Aggregate columns show 0 or NULL for records that should have high values
- Only a subset of records have the aggregate column populated
- Backfill scripts may have run for some records but not others
## Root Cause
Denormalized columns (copies of aggregated data stored for query performance) can become
stale when:
1. Initial data migration didn't populate them
2. Backfill scripts only ran for some records
3. New source records were added without updating the denormalized column
4. The aggregation logic changed but the column wasn't recalculated
## Solution
### Option 1: Compute at Query Time (Immediate Fix)
Join with the source table to compute actual aggregates:
```sql
-- BEFORE (broken): Uses potentially stale denormalized column
SELECT user_id, username
FROM users
WHERE status = 'pending'
ORDER BY total_loops DESC NULLS LAST;
-- AFTER (fixed): Computes actual aggregate from source
SELECT u.user_id, u.username,
COALESCE(SUM(vm.loops), 0) as actual_total_loops
FROM users u
LEFT JOIN vine_metadata vm ON u.user_id = vm.user_id
WHERE u.status = 'pending'
GROUP BY u.user_id, u.username
ORDER BY actual_total_loops DESC;
```
### Option 2: Backfill the Denormalized Column (Permanent Fix)
Update the denormalized column from the source data:
```sql
UPDATE users u
SET total_loops = subq.actual_loops
FROM (
SELECT user_id, COALESCE(SUM(loops), 0) as actual_loops
FROM vine_metadata
GROUP BY user_id
) subq
WHERE u.user_id = subq.user_id;
```
### Option 3: Use Materialized Views (Best of Both)
Create a materialized view for the aggregates:
```sql
CREATE MATERIALIZED VIEW user_stats AS
SELECT user_id,
COUNT(*) as item_count,
SUM(loops) as total_loops
FROM vine_metadata
GROUP BY user_id;
-- Refresh periodically
REFRESH MATERIALIZED VIEW user_stats;
```
## Verification
After applying the fix, verify the query returns expected results:
```sql
-- Check that top items are actually top items
SELECT user_id, username, actual_total_loops
FROM (your_fixed_query)
LIMIT 10;
-- Compare against direct aggregate
SELECT user_id, SUM(loops) as loops
FROM source_table
GROUP BY user_id
ORDER BY loops DESC
LIMIT 10;
```
## Example
**Scenario**: Avatar fetcher should process top Viners first (by total loops), but
instead processes users with ID prefix "10" (effectively random order).
**Investigation**:
```sql
-- Check if denormalized column is populated
SELECT
COUNT(*) as total,
SUM(CASE WHEN loop_count > 0 THEN 1 ELSE 0 END) as has_loop_count
FROM users;
-- Result: Only 29,878 of 119,785 users have loop_count populated
-- Check top users by denormalized vs actual
SELECT u.user_id, u.username, u.loop_count as denormalized,
SUM(vm.loops) as actual
FROM users u
JOIN vine_metadata vm ON u.user_id = vm.user_id
GROUP BY u.user_id, u.username, u.loop_count
ORDER BY SUM(vm.loops) DESC
LIMIT 5;
-- Result: Top creators show loop_count=0 but actual=1,281,730,353
```
**Fix**: Changed the query to JOIN with vine_metadata and ORDER BY the computed sum.
## Notes
- This is a classic denormalization trade-off: faster reads vs. stale data
- When denormalizing, always implement triggers or application-level updates to keep in sync
- Consider whether the aggregate query is fast enough to compute at runtime
- LEFT JOIN ensures records without source data still appear (with 0 values)
- Use `COALESCE(SUM(...), 0)` to handle NULL aggregates properly
- `NULLS LAST` in ORDER BY prevents NULL values from sorting first in DESC order
## Related Patterns
- Event sourcing: Keep source events, compute aggregates as needed
- CQRS: Separate read models that are explicitly updated
- Triggers: Automatically update denormalized columns on source changes
No comments yet. Be the first to comment!