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,
},
access_plan_label as planner_access_plan_label, plan_access_with_order,
},
schema::SchemaInfo,
},
model::{entity::EntityModel, index::IndexModel},
value::Value,
};
pub(in crate::db) use self::model::{
AccessChoiceCandidateExplainSummary, AccessChoiceExplainSnapshot, AccessChoiceResidualBurden,
};
#[must_use]
pub(in crate::db) fn project_access_choice_explain_snapshot_with_indexes(
model: &EntityModel,
visible_indexes: &[&'static IndexModel],
plan: &AccessPlannedQuery,
) -> AccessChoiceExplainSnapshot {
let (family, chosen_index_name, chosen_score_hint) =
chosen_access_shape_projection(&plan.access);
if matches!(family, AccessChoiceFamily::NonIndex) {
return plan.access_choice().clone();
}
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 candidates = 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 =>
{
candidates.push(project_candidate_explain_summary(
score,
&plan.access,
residual_burden_for_plan(plan),
));
}
self::model::CandidateEvaluation::Eligible(score) => {
alternatives.push(index_name);
eligible_other_scores.push(score);
if let Some(candidate_access) =
eligible_candidate_access_for_index(model, schema_info, plan, index)
{
let candidate_plan = candidate_plan_with_access(plan, candidate_access.clone());
candidates.push(project_candidate_explain_summary(
score,
&candidate_access,
residual_burden_for_plan(&candidate_plan),
));
}
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,
),
candidates,
alternatives,
rejected,
}
}
pub(in crate::db) fn non_index_access_choice_snapshot_for_access_plan<K>(
access: &AccessPlan<K>,
) -> AccessChoiceExplainSnapshot {
if access.selected_index_model().is_some() {
return AccessChoiceExplainSnapshot::selected_index_not_projected();
}
if access.as_by_key_path().is_some() {
return AccessChoiceExplainSnapshot {
chosen_reason: self::model::AccessChoiceSelectedReason::ByKeyAccess,
candidates: Vec::new(),
alternatives: Vec::new(),
rejected: Vec::new(),
};
}
if access
.as_path()
.and_then(|path| path.as_by_keys())
.is_some()
{
return AccessChoiceExplainSnapshot {
chosen_reason: self::model::AccessChoiceSelectedReason::ByKeysAccess,
candidates: Vec::new(),
alternatives: Vec::new(),
rejected: Vec::new(),
};
}
if access.as_primary_key_range_path().is_some() {
return AccessChoiceExplainSnapshot {
chosen_reason: self::model::AccessChoiceSelectedReason::PrimaryKeyRangeAccess,
candidates: Vec::new(),
alternatives: Vec::new(),
rejected: Vec::new(),
};
}
if access.is_single_full_scan() {
return AccessChoiceExplainSnapshot {
chosen_reason: self::model::AccessChoiceSelectedReason::FullScanAccess,
candidates: Vec::new(),
alternatives: Vec::new(),
rejected: Vec::new(),
};
}
AccessChoiceExplainSnapshot::non_index_access()
}
#[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,
}
impl ResidualBurdenProfile {
const fn kind(self) -> AccessChoiceResidualBurden {
match self.kind_rank {
0 => AccessChoiceResidualBurden::None,
1 => AccessChoiceResidualBurden::PredicateOnly,
_ => AccessChoiceResidualBurden::ScalarExpression,
}
}
}
#[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 eligible_candidate_access_for_index(
model: &EntityModel,
schema_info: &SchemaInfo,
plan: &AccessPlannedQuery,
index: &'static IndexModel,
) -> Option<AccessPlan<Value>> {
plan_access_with_order(
model,
&[index],
schema_info,
plan.scalar_plan().predicate.as_ref(),
plan.scalar_plan().order.as_ref(),
plan.grouped_plan().is_some(),
)
.ok()
}
fn candidate_plan_with_access(
plan: &AccessPlannedQuery,
access: AccessPlan<Value>,
) -> AccessPlannedQuery {
AccessPlannedQuery::from_parts_with_projection(
plan.logical.clone(),
access,
plan.projection_selection.clone(),
)
}
fn project_candidate_explain_summary(
score: crate::db::query::plan::planner::AccessCandidateScore,
access: &AccessPlan<Value>,
residual_burden: ResidualBurdenProfile,
) -> AccessChoiceCandidateExplainSummary {
AccessChoiceCandidateExplainSummary {
label: planner_access_plan_label(access),
exact: score.exact,
filtered: score.filtered,
range_bound_count: usize::from(score.range_bound_count),
order_compatible: score.order_compatible,
residual_burden: residual_burden.kind(),
residual_predicate_terms: residual_burden.predicate_term_count,
}
}
fn same_score_competing_candidate_plans(
model: &EntityModel,
visible_indexes: &[&'static IndexModel],
schema_info: &SchemaInfo,
plan: &AccessPlannedQuery,
) -> Option<Vec<ResidualComparableCandidate>> {
let (family, chosen_index_name, chosen_score_hint) =
chosen_access_shape_projection(&plan.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 =
eligible_candidate_access_for_index(model, schema_info, plan, index)?;
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 = candidate_plan_with_access(plan, candidate_access.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,
}
}