mod evaluator;
mod model;
#[cfg(test)]
mod tests;
use crate::{
db::{
access::AccessPlan,
predicate::Predicate,
query::plan::{
AccessPlannedQuery,
access_choice::{
evaluator::{
chosen_access_shape_projection, chosen_selection_reason,
evaluate_index_candidate, ranked_rejection_reason, sorted_indexes,
},
model::AccessChoiceFamily,
},
plan_access_with_order,
},
schema::SchemaInfo,
},
model::{entity::EntityModel, index::IndexModel},
value::Value,
};
pub(in crate::db) use self::model::AccessChoiceExplainSnapshot;
#[must_use]
pub(in crate::db) fn project_access_choice_explain_snapshot_with_indexes(
model: &EntityModel,
visible_indexes: &[&'static IndexModel],
plan: &AccessPlannedQuery,
) -> AccessChoiceExplainSnapshot {
let access = crate::db::query::explain::ExplainAccessPath::from_access_plan(&plan.access);
let (family, chosen_index_name, chosen_score_hint) = chosen_access_shape_projection(&access);
if matches!(family, AccessChoiceFamily::NonIndex) {
return AccessChoiceExplainSnapshot::non_index_access();
}
let Some(chosen_index_name) = chosen_index_name else {
return AccessChoiceExplainSnapshot::selected_index_not_projected();
};
let schema_info = SchemaInfo::cached_for_entity_model(model);
let predicate = plan.scalar_plan().predicate.as_ref();
let order = plan.scalar_plan().order.as_ref();
let grouped = plan.grouped_plan().is_some();
let chosen_score = visible_indexes
.iter()
.copied()
.find(|index| index.name() == chosen_index_name)
.and_then(|index| {
match evaluate_index_candidate(
family,
index,
model,
schema_info,
predicate,
order,
grouped,
) {
self::model::CandidateEvaluation::Eligible(score) => Some(score),
self::model::CandidateEvaluation::Rejected(_) => None,
}
})
.unwrap_or(chosen_score_hint);
let mut alternatives = Vec::new();
let mut rejected = Vec::new();
let mut eligible_other_scores = Vec::new();
let residual_burden_rejected_indexes =
same_score_competing_residual_rejection_indexes(model, visible_indexes, schema_info, plan);
for index in sorted_indexes(visible_indexes) {
let index_name = index.name();
match evaluate_index_candidate(family, index, model, schema_info, predicate, order, grouped)
{
self::model::CandidateEvaluation::Eligible(_score)
if index_name == chosen_index_name => {}
self::model::CandidateEvaluation::Eligible(score) => {
alternatives.push(index_name);
eligible_other_scores.push(score);
let rejected_on_residual_burden = residual_burden_rejected_indexes
.as_ref()
.is_some_and(|indexes| indexes.contains(&index_name));
rejected.push(
ranked_rejection_reason(
family,
score,
chosen_score,
rejected_on_residual_burden,
)
.render_for_index(index_name),
);
}
self::model::CandidateEvaluation::Rejected(reason) => {
rejected.push(reason.render_for_index(index_name));
}
}
}
let residual_burden_preferred =
chosen_access_prefers_lower_residual_burden(model, visible_indexes, schema_info, plan);
AccessChoiceExplainSnapshot {
chosen_reason: chosen_selection_reason(
family,
chosen_score,
&eligible_other_scores,
residual_burden_preferred,
),
alternatives,
rejected,
}
}
#[must_use]
pub(in crate::db::query) fn rerank_access_plan_by_residual_burden_with_indexes(
model: &EntityModel,
visible_indexes: &[&'static IndexModel],
schema_info: &SchemaInfo,
plan: &AccessPlannedQuery,
) -> Option<AccessPlan<Value>> {
let preferred = preferred_same_score_competing_access_by_residual_burden(
model,
visible_indexes,
schema_info,
plan,
)?;
Some(preferred.access)
}
fn chosen_access_prefers_lower_residual_burden(
model: &EntityModel,
visible_indexes: &[&'static IndexModel],
schema_info: &SchemaInfo,
plan: &AccessPlannedQuery,
) -> bool {
preferred_same_score_competing_access_by_residual_burden(
model,
visible_indexes,
schema_info,
plan,
)
.is_none()
&& same_score_competing_candidate_plans(model, visible_indexes, schema_info, plan)
.into_iter()
.flatten()
.any(|candidate| candidate.residual_burden > residual_burden_for_plan(plan))
}
fn same_score_competing_residual_rejection_indexes(
model: &EntityModel,
visible_indexes: &[&'static IndexModel],
schema_info: &SchemaInfo,
plan: &AccessPlannedQuery,
) -> Option<Vec<&'static str>> {
let chosen_burden = residual_burden_for_plan(plan);
let rejected = same_score_competing_candidate_plans(model, visible_indexes, schema_info, plan)?
.into_iter()
.filter(|candidate| candidate.residual_burden > chosen_burden)
.filter_map(|candidate| {
candidate
.access
.selected_index_model()
.map(IndexModel::name)
})
.collect::<Vec<_>>();
(!rejected.is_empty()).then_some(rejected)
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
struct ResidualBurdenProfile {
kind_rank: u8,
predicate_term_count: usize,
}
#[derive(Clone, Debug)]
struct ResidualComparableCandidate {
access: AccessPlan<Value>,
residual_burden: ResidualBurdenProfile,
}
fn preferred_same_score_competing_access_by_residual_burden(
model: &EntityModel,
visible_indexes: &[&'static IndexModel],
schema_info: &SchemaInfo,
plan: &AccessPlannedQuery,
) -> Option<ResidualComparableCandidate> {
let chosen_burden = residual_burden_for_plan(plan);
let mut best: Option<ResidualComparableCandidate> = None;
for candidate in same_score_competing_candidate_plans(model, visible_indexes, schema_info, plan)
.into_iter()
.flatten()
{
if candidate.residual_burden >= chosen_burden {
continue;
}
match &best {
None => best = Some(candidate),
Some(existing) if candidate.residual_burden < existing.residual_burden => {
best = Some(candidate);
}
Some(_) => {}
}
}
best
}
fn same_score_competing_candidate_plans(
model: &EntityModel,
visible_indexes: &[&'static IndexModel],
schema_info: &SchemaInfo,
plan: &AccessPlannedQuery,
) -> Option<Vec<ResidualComparableCandidate>> {
let access = crate::db::query::explain::ExplainAccessPath::from_access_plan(&plan.access);
let (family, chosen_index_name, chosen_score_hint) = chosen_access_shape_projection(&access);
if matches!(family, AccessChoiceFamily::NonIndex) {
return None;
}
let chosen_index_name = chosen_index_name?;
let predicate = plan.scalar_plan().predicate.as_ref();
let order = plan.scalar_plan().order.as_ref();
let grouped = plan.grouped_plan().is_some();
let chosen_score = visible_indexes
.iter()
.copied()
.find(|index| index.name() == chosen_index_name)
.and_then(|index| {
match evaluate_index_candidate(
family,
index,
model,
schema_info,
predicate,
order,
grouped,
) {
self::model::CandidateEvaluation::Eligible(score) => Some(score),
self::model::CandidateEvaluation::Rejected(_) => None,
}
})
.unwrap_or(chosen_score_hint);
let mut candidates = Vec::new();
for index in sorted_indexes(visible_indexes) {
if index.name() == chosen_index_name {
continue;
}
let self::model::CandidateEvaluation::Eligible(score) =
evaluate_index_candidate(family, index, model, schema_info, predicate, order, grouped)
else {
continue;
};
if score != chosen_score {
continue;
}
let candidate_access =
plan_access_with_order(model, &[index], schema_info, predicate, order, grouped).ok()?;
let candidate_access_name = candidate_access
.selected_index_model()
.map(crate::model::index::IndexModel::name);
if candidate_access_name != Some(index.name()) {
continue;
}
let candidate_plan = AccessPlannedQuery::from_parts_with_projection(
plan.logical.clone(),
candidate_access.clone(),
plan.projection_selection.clone(),
);
candidates.push(ResidualComparableCandidate {
access: candidate_access,
residual_burden: residual_burden_for_plan(&candidate_plan),
});
}
Some(candidates)
}
fn residual_burden_for_plan(plan: &AccessPlannedQuery) -> ResidualBurdenProfile {
let predicate_term_count = plan
.effective_execution_predicate()
.as_ref()
.map_or(0, count_predicate_terms);
let kind_rank = if plan.residual_filter_expr().is_some() {
2
} else {
u8::from(predicate_term_count > 0)
};
ResidualBurdenProfile {
kind_rank,
predicate_term_count,
}
}
fn count_predicate_terms(predicate: &Predicate) -> usize {
match predicate {
Predicate::And(children) | Predicate::Or(children) => {
children.iter().map(count_predicate_terms).sum()
}
Predicate::True | Predicate::False => 0,
Predicate::Not(_)
| Predicate::Compare(_)
| Predicate::CompareFields(_)
| Predicate::IsNull { .. }
| Predicate::IsNotNull { .. }
| Predicate::IsMissing { .. }
| Predicate::IsEmpty { .. }
| Predicate::IsNotEmpty { .. }
| Predicate::TextContains { .. }
| Predicate::TextContainsCi { .. } => 1,
}
}