count_unconsolidated() seq-scans memory_units on every consolidation job (inner LIMIT has no ORDER BY)
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 83/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- postgresql, python
- Domain
- databases, performance
Research direction
Read hindsight-api-slim/hindsight_api/engine/memories/pg/reads.py around count_unconsolidated() and compare its query with find_unconsolidated(). The issue includes SQL to reproduce the planner behavior; verify the change with the provided EXPLAIN (ANALYZE, COSTS OFF) queries. Done when the bounded count uses the expected index scan while returning the same capped count.
Written by the indexing model from the issue text.
Description
Bug Description
count_unconsolidated() seq-scans the whole memory_units table on every consolidation job, even when nothing is pending. The consolidator calls it at the start of each job for progress logging (consolidator.py#L1929).
Cause: the bounded count in pg/reads.py#L444-L449 has a LIMIT but no ORDER BY:
SELECT COUNT(*) AS c FROM (
SELECT 1 FROM memory_units
WHERE bank_id = $1 AND consolidated_at IS NULL AND consolidation_failed_at IS NULL
AND fact_type = ANY($2)
LIMIT $3
) sub
The planner overestimates the pending rows (about 81k estimated, 0 actual), so "seq scan until LIMIT rows are found" looks cheap. With nothing pending, the scan reads the whole table. find_unconsolidated() has the same predicates plus ORDER BY created_at ASC (pg/reads.py#L402). That ordering matches idx_memory_units_unconsolidated (bank_id, created_at), so find_unconsolidated() gets an index scan.
Suggested fix: add ORDER BY created_at to the inner query of count_unconsolidated(). The count is unchanged, because any LIMIT rows give the same capped count.
Steps to Reproduce
On a bank with about 354k memory_units rows (PostgreSQL 18, embedded pg0, Hindsight 1.2.0), with nothing pending and no extra indexes beyond the shipped ones:
PREPARE cur(text, text[], int) AS
SELECT COUNT(*) AS c FROM (SELECT 1 FROM memory_units
WHERE bank_id = $1 AND consolidated_at IS NULL AND consolidation_failed_at IS NULL
AND fact_type = ANY($2) LIMIT $3) sub;
PREPARE fix(text, text[], int) AS
SELECT COUNT(*) AS c FROM (SELECT 1 FROM memory_units
WHERE bank_id = $1 AND consolidated_at IS NULL AND consolidation_failed_at IS NULL
AND fact_type = ANY($2) ORDER BY created_at LIMIT $3) sub;
SET plan_cache_mode = force_custom_plan;
EXPLAIN (ANALYZE, COSTS OFF) EXECUTE cur('<bank>', '{experience,world}', 1000);
EXPLAIN (ANALYZE, COSTS OFF) EXECUTE fix('<bank>', '{experience,world}', 1000);
Expected Behavior
An index scan on idx_memory_units_unconsolidated, as find_unconsolidated() gets.
Actual Behavior
| Query | Plan | Execution |
|---|---|---|
current (cur) |
Seq Scan on memory_units | 227 ms |
with ORDER BY created_at (fix) |
Index Scan using idx_memory_units_unconsolidated | 0.1 ms |
With force_generic_plan, both shapes fall back to a seq scan (the fix shape adds a sort). Postgres only picks a generic plan when it is not more expensive than the custom plans, so in practice fix stays on the index plan.
Version
1.2.0 (same code on main at eb963cb5a929d92c081371df64c2d5c2fe3351af)
- Dominant language
- Python
- Stars
- 45.2k
- Forks
- 5.9k
- Avg merge
- 3h 44m
- Merged PRs (30d)
- 491
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from vectorize-io/hindsight
-
core
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
vectorize-io/hindsight#6430 ·
Maintainers usually reply within 1 day
-
core
Difficulty 1/5 Under an hour Newbie friendliness 83/100
vectorize-io/hindsight#6427 ·
Maintainers usually reply within 1 day
-
core
Difficulty 1/5 Under an hour Newbie friendliness 75/100
vectorize-io/hindsight#6416 ·
Maintainers usually reply within 1 day
-
core
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
vectorize-io/hindsight#6397 ·
Maintainers usually reply within 1 day
-
core
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
vectorize-io/hindsight#6396 ·
Maintainers usually reply within 1 day
All issues in vectorize-io/hindsight
Similar issues
-
ChatBedrockConverse changes the keys of JSON tool results to camelCase (`order_id` becomes `orderId`)Possibly taken @geyazheng claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
langchain-ai/langchain-aws#1469 ·
Maintainers usually reply within 1 day
-
GCToggler leaves collection enabled if the body re-enables itPossibly taken @Gambit-Checkmate claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Maintainers usually reply within 7 days
-
Difficulty 1/5 Under an hour Newbie friendliness 88/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
doronz88/pymobiledevice3#2176 ·
Maintainers usually reply within 1 day
-
b4b usability
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Maintainers usually reply within 1 day