OpenCATS still contains a number of database/query patterns inherited from the original application which are either inefficient on larger datasets, deprecated, or more complicated than necessary with modern MariaDB.
This issue tracks incremental improvements to the database layer while preserving existing OpenCATS behaviour.
The work will be split into small, independently reviewable PRs rather than implemented as one large refactor.
Database baseline
For this work:
- MariaDB 10.7 remains the known-good reference database.
- Existing CI already runs against MariaDB 10.7.
- Do not change the MariaDB version as part of this work.
- MariaDB versions newer than 10.7 have previously demonstrated behavioural/startup differences and should be investigated separately.
- Do not add MySQL compatibility work to this issue.
- Avoid deliberately introducing unnecessary MySQL incompatibility, but MariaDB behaviour is the reference for testing and optimisation.
- PHP 8.4.1 and PHP 8.5 remain the supported PHP CI targets.
General principles
Phase 1 — Establish regression coverage
Before changing the larger queries, improve CI coverage around the behaviour we intend to preserve.
DataGrid behaviour
Candidate behaviour
Job Order behaviour
Activity behaviour
Search behaviour
Existing DatabaseSearchTest primarily validates generated SQL strings.
These tests should exist before the search subsystem itself is modernised.
Phase 2 — Low-risk query and index improvements
Start with changes which do not materially restructure the existing DataGrid architecture.
Activity UNION ALL
Current ActivityDataGrid combines Candidate and Contact queries using UNION.
The two result sets are inherently separated by data_item_type, therefore duplicate elimination should not normally be necessary.
Review missing composite indexes
Evaluate the following against current master queries and a realistic database.
Do not add them solely because they improved the older PHP 5.4 installation.
Candidate latest pipeline/status
Evaluate:
(candidate_id, date_modified)
on candidate_joborder.
Current Candidate queries frequently locate a candidate's most recently modified pipeline record.
Job Order pipeline
Evaluate:
on candidate_joborder.
Potentially also evaluate:
(joborder_id, candidate_id)
where EXISTS/membership checks require it.
Job Order status history
Evaluate:
on candidate_joborder_status_history.
Current schema only provides a status_to index despite JobOrder queries commonly filtering by both values.
Extra fields
Evaluate:
(data_item_type, field_name, data_item_id)
on extra_field.
Current ExtraFields DataGrid joins use all three values.
Activity
Do not carry forward the old:
(data_item_type, joborder_id, data_item_id, date_created)
index unchanged.
Current ActivityDataGrid uses date_occurred.
Evaluate an index based around the current access pattern, potentially:
(data_item_type, date_occurred, activity_id)
The final definition should follow the actual optimised query plan.
Phase 3 — DataGrid count-query infrastructure
Introduce a backward-compatible mechanism allowing a DataGrid query to supply both its data query and its count query.
For example:
return array($dataSQL, $countSQL);
DataGrid should:
No existing grid should be converted in this infrastructure PR.
Add CI tests proving:
This provides a gradual migration path without requiring a breaking DataGrid rewrite.
Phase 4 — Candidate query optimisation
Candidates are likely to provide one of the largest practical improvements on established OpenCATS installations.
Latest pipeline/status
Current Candidate DataGrid uses correlated queries similar to:
WHERE candidate_joborder.candidate_id = candidate.candidate_id
ORDER BY candidate_joborder.date_modified DESC
LIMIT 1
Initial work:
Later, if required:
Preserve exactly the same Candidate DataGrid output.
Replace presence joins/counts with EXISTS
Where OpenCATS only needs a boolean answer, avoid producing/aggregating unnecessary rows.
Potential candidates include:
Use:
where it produces a simpler and better plan.
Phase 5 — Job Order pipeline aggregation
Current JobOrders DataGrid repeatedly executes correlated aggregate queries for values such as:
- total pipeline;
- not contacted;
- submitted;
- interviewing.
Replace these repeated operations with grouped aggregate relations.
For example, conceptually:
SELECT
joborder_id,
COUNT(*) AS pipeline,
SUM(status IN (...)) AS notContacted
FROM candidate_joborder
GROUP BY joborder_id
joined once to the JobOrder query.
Prefer a simple derived table unless another MariaDB construct provides a clear benefit.
Phase 6 — Remove SQL_CALC_FOUND_ROWS
Once the two-query DataGrid infrastructure is established, migrate grids individually.
Current users include:
Each migration should:
- execute the normal result query with its
LIMIT;
- execute a separate lightweight count query;
- preserve existing filters;
- preserve current
HAVING behaviour;
- avoid display-only joins/calculations in the count query where possible;
- prove identical row counts and pagination in CI.
After all callers are migrated:
Phase 7 — Make DataGrid filters index-friendly
Some current DataGrid filtering operates on formatted values using HAVING, for example:
HAVING DATE_FORMAT(candidate.date_modified, '%m-%d-%y') = ...
Where the filter is not genuinely aggregate-dependent:
For a single date, prefer a range such as:
WHERE date_modified >= '2026-08-28 00:00:00'
AND date_modified < '2026-08-29 00:00:00'
This should allow existing datetime indexes to participate in the query.
Apply this incrementally per DataGrid.
Phase 8 — Page IDs before expensive display calculations
For DataGrids which remain expensive after the earlier improvements:
Ensure:
Do not implement this through regex/string manipulation of generated SQL.
Each DataGrid should explicitly define its query because each grid has different joins, filters and computed columns.
Phase 9 — Search subsystem modernisation
DatabaseSearch.php contains a substantial custom search-language implementation built around:
- AND / OR / NOT parsing;
- quoted values;
- wildcard handling;
- SQL
REGEXP;
%LIKE%;
- custom full-text encoding.
Treat this as a separate substantial change.
First: preserve existing behaviour
Then evaluate simplification
Do not replace the current search implementation unless its user-visible behaviour is either preserved or intentionally changed and documented.
Phase 10 — Simplify database writes
Several older OpenCATS code paths perform:
DELETE existing value
INSERT replacement value
Examples include settings and extra-field values.
Evaluate:
Treat this separately from DataGrid performance work.
Production-size performance validation
Use a copy of a genuine OpenCATS 0.9.4 production database.
CI should prove correctness.
The upgraded production-size database should prove performance.
Suggested implementation order
Out of scope
The following should be separate issues:
- upgrading MariaDB beyond 10.7;
- investigating the known behaviour differences in later MariaDB releases;
- adding/supporting MySQL as an additional CI database;
- frontend/UI modernisation;
- major database abstraction/framework changes.
OpenCATS still contains a number of database/query patterns inherited from the original application which are either inefficient on larger datasets, deprecated, or more complicated than necessary with modern MariaDB.
This issue tracks incremental improvements to the database layer while preserving existing OpenCATS behaviour.
The work will be split into small, independently reviewable PRs rather than implemented as one large refactor.
Database baseline
For this work:
General principles
EXPLAIN/ANALYZEand slow-query evidence rather than assuming a rewrite is faster.Phase 1 — Establish regression coverage
Before changing the larger queries, improve CI coverage around the behaviour we intend to preserve.
DataGrid behaviour
HAVING.Candidate behaviour
Job Order behaviour
Activity behaviour
Search behaviour
Existing
DatabaseSearchTestprimarily validates generated SQL strings.These tests should exist before the search subsystem itself is modernised.
Phase 2 — Low-risk query and index improvements
Start with changes which do not materially restructure the existing DataGrid architecture.
Activity
UNION ALLCurrent ActivityDataGrid combines Candidate and Contact queries using
UNION.The two result sets are inherently separated by
data_item_type, therefore duplicate elimination should not normally be necessary.UNIONwithUNION ALL.Review missing composite indexes
Evaluate the following against current master queries and a realistic database.
Do not add them solely because they improved the older PHP 5.4 installation.
Candidate latest pipeline/status
Evaluate:
on
candidate_joborder.Current Candidate queries frequently locate a candidate's most recently modified pipeline record.
candidate_idanddate_modifiedindexes.ANALYZEdemonstrates a useful improvement.Job Order pipeline
Evaluate:
on
candidate_joborder.Potentially also evaluate:
where
EXISTS/membership checks require it.Job Order status history
Evaluate:
on
candidate_joborder_status_history.Current schema only provides a
status_toindex despite JobOrder queries commonly filtering by both values.Extra fields
Evaluate:
on
extra_field.Current ExtraFields DataGrid joins use all three values.
Activity
Do not carry forward the old:
index unchanged.
Current ActivityDataGrid uses
date_occurred.Evaluate an index based around the current access pattern, potentially:
The final definition should follow the actual optimised query plan.
Phase 3 — DataGrid count-query infrastructure
Introduce a backward-compatible mechanism allowing a DataGrid query to supply both its data query and its count query.
For example:
DataGridshould:FOUND_ROWS()path for grids not yet migrated.No existing grid should be converted in this infrastructure PR.
Add CI tests proving:
This provides a gradual migration path without requiring a breaking DataGrid rewrite.
Phase 4 — Candidate query optimisation
Candidates are likely to provide one of the largest practical improvements on established OpenCATS installations.
Latest pipeline/status
Current Candidate DataGrid uses correlated queries similar to:
Initial work:
(candidate_id, date_modified)composite index;Later, if required:
ROW_NUMBER()only if it improves clarity/performance and is appropriate for the MariaDB 10.7 baseline.Preserve exactly the same Candidate DataGrid output.
Replace presence joins/counts with
EXISTSWhere OpenCATS only needs a boolean answer, avoid producing/aggregating unnecessary rows.
Potential candidates include:
Use:
EXISTS ( SELECT 1 ... )where it produces a simpler and better plan.
Phase 5 — Job Order pipeline aggregation
Current JobOrders DataGrid repeatedly executes correlated aggregate queries for values such as:
Replace these repeated operations with grouped aggregate relations.
For example, conceptually:
joined once to the JobOrder query.
candidate_joborderonce.candidate_joborder_status_historyonce.Prefer a simple derived table unless another MariaDB construct provides a clear benefit.
Phase 6 — Remove
SQL_CALC_FOUND_ROWSOnce the two-query DataGrid infrastructure is established, migrate grids individually.
Current users include:
SQL_CALC_FOUND_ROWSqueriesEach migration should:
LIMIT;HAVINGbehaviour;After all callers are migrated:
FOUND_ROWS()fallback fromDataGrid;SQL_CALC_FOUND_ROWS.Phase 7 — Make DataGrid filters index-friendly
Some current DataGrid filtering operates on formatted values using
HAVING, for example:Where the filter is not genuinely aggregate-dependent:
HAVINGtoWHERE;DATE_FORMAT()only for presentation.For a single date, prefer a range such as:
This should allow existing datetime indexes to participate in the query.
Apply this incrementally per DataGrid.
Phase 8 — Page IDs before expensive display calculations
For DataGrids which remain expensive after the earlier improvements:
LIMITto that smaller result;Ensure:
LIMIT;LIMIT;Do not implement this through regex/string manipulation of generated SQL.
Each DataGrid should explicitly define its query because each grid has different joins, filters and computed columns.
Phase 9 — Search subsystem modernisation
DatabaseSearch.phpcontains a substantial custom search-language implementation built around:REGEXP;%LIKE%;Treat this as a separate substantial change.
First: preserve existing behaviour
Then evaluate simplification
FULLTEXTsupport.MATCH() ... AGAINST().Do not replace the current search implementation unless its user-visible behaviour is either preserved or intentionally changed and documented.
Phase 10 — Simplify database writes
Several older OpenCATS code paths perform:
Examples include settings and extra-field values.
Evaluate:
INSERT ... ON DUPLICATE KEY UPDATE;Treat this separately from DataGrid performance work.
Production-size performance validation
Use a copy of a genuine OpenCATS 0.9.4 production database.
EXPLAIN/ANALYZEresults before changes.CI should prove correctness.
The upgraded production-size database should prove performance.
Suggested implementation order
UNION→UNION ALLSQL_CALC_FOUND_ROWSgrid-by-gridHAVINGexpressions toWHEREOut of scope
The following should be separate issues: