Verified against SharpCoreDB 2.1.0. The optimizer on the engine's query path is
SharpCoreDB.Optimization.QueryOptimizer(src/SharpCoreDB/Optimization/QueryOptimizer.cs), constructed withOptimization.CostEstimatorfromServices/QueryCompiler.cs. ThePlanning/types (QueryOptimizer,CardinalityEstimator,PredicatePushdown) andQuery/CostBasedOptimizer.csare real, compiling components that no shipped call site constructs — building blocks, not the active path. Sections that used to describe components which do not exist at all (SubqueryOptimizer,JoinReorderer,OptimizerCache,QueryOptimizerExtensions,OptimizerIntegration,InvalidateOptimizationCache()) are corrected below and marked as such.
A lightweight, cost-based query optimizer for SharpCoreDB with logical/physical plan separation.
Key Characteristics:
- ✅ Cost-based plan selection
- ✅
readonly structplan nodes, no LINQ and no async on the plan path - ✅ Plans cached per compiled query (
Database/Caching/Database.PlanCaching.cs) ⚠️ Join reordering and subquery elimination are not implemented on this path — see §4 and §6
Parse AST → Logical view (shape flags, estimated rows) → Costed plan → Physical steps → Execute
one pass, no rewrite phases
Optimize(SelectNode) runs three steps, in this order:
1. Logical view (BuildLogicalPlan) — derives shape flags from the AST and an estimated row count
from CostEstimator.EstimateScanCost. It does not rewrite the query:
PredicatesPushed = hasWhere || hasHaving ("pushdown planned"), JoinsReordered = hasJoins
("placeholder flag"), SubqueriesEliminated = !hasSubquery ("trivially eliminated"),
Cacheable = !hasSubquery.
2. Costing (EstimateCost) — accumulates a heuristic cost per recognised clause (base scan, then
joins, filters, aggregates, sort) in clause order.
3. Physical steps (BuildPhysicalSteps) — emits one PhysicalStep per clause it recognises:
TableScan, Filter, Aggregate, Sort, Project.
Not part of this pipeline (they were described here as phases 2-4 in earlier revisions): predicate pushdown as a rewrite,
EXISTS/NOT EXISTS/IN (SELECT)conversion to semi/anti-joins, and join reordering. The first is only flagged, the second has no implementation (SemiJoinandAntiJoindo not exist in the source), and the third has algorithms on two components the engine does not construct. §"Optimization rules" is the authoritative table.
Optimization.QueryOptimizer ← the engine's query path (Services/QueryCompiler.cs)
├── Optimization.CostEstimator (scan/filter/join/aggregate/sort costs)
└── Optimization.PhysicalStepType (TableScan, Filter, Aggregate, Sort, Project)
Database/Caching/Database.PlanCaching.cs ← compiled-plan cache (normalised SQL + parameter shape)
└── Services.QueryPlanCache (ConcurrentDictionary + LRU)
Planning/{QueryOptimizer,CardinalityEstimator,PredicatePushdown} ← building blocks, not wired in
Query/CostBasedOptimizer ← building block, not wired in
Execution/{JoinExecutor,SubqueryCache,AggregationOptimizer} ← execution side
Main entry point: QueryOptimizer.Optimize(SelectNode query) → PhysicalPlan
Constructed as: new QueryOptimizer(new CostEstimator(stats)) in Services/QueryCompiler.cs
Key types (all in src/SharpCoreDB/Optimization/QueryOptimizer.cs):
LogicalPlan(readonly struct) —Query,PredicatesPushed,JoinsReordered,SubqueriesEliminated,EstimatedRows,CacheablePhysicalPlan(readonly struct) —Logical,Steps(PhysicalStep[]),EstimatedCostPhysicalStep— one execution step:Type,TableName,AliasPhysicalStepType—TableScan,Filter,Aggregate,Sort,Project
What it actually does: walks the parsed SelectNode and emits one physical step per clause it
recognises (scan, filter, aggregate, sort, project), pricing the plan through CostEstimator. Two
LogicalPlan flags describe the query's shape, not the result of a rewrite:
JoinsReordered = hasJoins and SubqueriesEliminated = !hasSubquery (the assignments in Optimize
carry the comment "placeholder flag" / "trivially eliminated").
Example:
var optimizer = new QueryOptimizer(new CostEstimator(tableStats));
var physicalPlan = optimizer.Optimize(selectNode);
Console.WriteLine($"cost={physicalPlan.EstimatedCost} steps={physicalPlan.Steps.Length}");
foreach (var step in physicalPlan.Steps)
{
Console.WriteLine($"{step.Type}: {step.TableName ?? "N/A"}");
}
// Steps follow the statement's clause order, e.g.
// TableScan(orders) → Filter(id > 5) → Aggregate → Sort → ProjectPurpose: prices a plan and predicts its output size.
Real API (src/SharpCoreDB/Optimization/CostEstimator.cs):
var estimator = new CostEstimator(statistics); // IReadOnlyDictionary<string, TableStatistics>
EstimatedCost scan = estimator.EstimateScanCost("orders");
EstimatedCost filter = estimator.EstimateFilterCost(inputCost);
EstimatedCost join = estimator.EstimateJoinCost(leftCost, rightCost, /* … */);
EstimatedCost agg = estimator.EstimateAggregateCost(inputCost);
EstimatedCost sort = estimator.EstimateSortCost(inputCost);
Console.WriteLine($"cost={scan.Cost} rows={scan.OutputRows}"); // EstimatedCost { double Cost; long OutputRows; }The coefficients that turn each operator into Cost/OutputRows are in the source file — read them
there rather than trusting a copied table; they are the numbers the planner compares.
Purpose: apply filters as close to the storage layer as possible, and evaluate the checklist of predicates most-selective-first.
Correction: this is a building block, not a wired-in rewrite.
Planning/PredicatePushdown.csexposes static helpers and is referenced by no shipped call site; on the compiled path the optimizer only records the intent —LogicalPlan.PredicatesPushed = hasWhere || hasHaving, commented "pushdown planned" inOptimization/QueryOptimizer.cs. The compiled plan carries the actual filter delegates (whereFilter,whereFilterIndexed) built inServices/QueryCompiler.cs.
Real API (src/SharpCoreDB/Planning/PredicatePushdown.cs, all static):
// Order predicates most-selective-first before evaluating them.
List<PredicateInfo> ordered = PredicatePushdown.OptimizePredicateOrder(predicates);
// Can this predicate be evaluated at this level (are its columns available)?
bool pushable = PredicatePushdown.CanPushDown(predicate, availableColumns);
// Evaluate pushed predicates against columnar data and return the surviving row indices.
// Integrates with the SIMD filter path (column encoding aware).
int[] rows = PredicatePushdown.ExecutePushedPredicates(
predicates, totalRows, getColumnData, getColumnEncoding);
// Rewrite a predicate for columnar access; combine AND-connected predicates.
PredicateInfo rewritten = PredicatePushdown.RewriteForColumnar(predicate, /* … */);
List<PredicateInfo> combined = PredicatePushdown.CombinePredicates(predicates);The PushdownPredicates(logicalPlan) / new PredicatePushdown() shape shown in earlier revisions of
this page does not exist.
Correction: there is no
SubqueryOptimizerand noEliminateSubqueries(...); the typesSemiJoinandAntiJoindo not exist anywhere in the source. SharpCoreDB does not rewriteEXISTS/NOT EXISTS/IN (SELECT)into semi-joins or anti-joins.LogicalPlan.SubqueriesEliminatedis!hasSubquery— a shape flag, commented "non-subquery sources are trivially eliminated".
What does exist (src/SharpCoreDB/Execution/):
| Type | Role |
|---|---|
SubqueryClassifier |
classifies the subquery (shape/type) |
SubqueryPlanner |
turns it into an execution plan |
SubqueryExecutor |
executes it |
SubqueryCache |
caches the result, with per-table invalidation |
var cached = cache.GetOrExecute(/* … */); // result cache
cache.Invalidate("orders"); // drop entries for one table
cache.Clear();
var (hits, misses, hitRate, count) = cache.GetStatistics();Non-correlated subqueries therefore benefit from result caching rather than from a plan rewrite; correlated ones are executed as the plan dictates.
Correction: there is no
JoinReorderertype and noReorderJoins(...)method. The compiled path does not reorder joins:LogicalPlan.JoinsReordered = hasJoins, commented "placeholder flag". Joins execute throughExecution/JoinExecutor.cs.
OptimizeJoinOrder(...) does exist, on two components that no shipped call site constructs:
// Planning/QueryOptimizer.cs — statistics-driven, accepts TableInfo{Name, RowCount}
List<string> order = plannerOptimizer.OptimizeJoinOrder(tables, joinConditions);
// Query/CostBasedOptimizer.cs — table-name list + registered TableStatistics
List<string> order2 = costBasedOptimizer.OptimizeJoinOrder(tableNames, joins);Execution reality (src/SharpCoreDB/Execution/JoinExecutor.cs): the join executor selects between a
hash join and a nested-loop join on row count, but the hash-join threshold is currently
HashJoinThreshold = int.MaxValue with the comment "Disable hash join until key hashing is
implemented" — so in practice joins run as nested-loop joins today.
Correction:
OptimizerCache,QueryOptimizerExtensions,OptimizerIntegration.GetOrOptimize(...)andInvalidateOptimizationCache()do not exist. Plan caching is done by the compiled-plan cache.
Database.ExecuteSQL / ExecuteQuery
│
▼
Database/Caching/Database.PlanCaching.cs ← GetOrAddPlan / TryGetCachedPlan /
│ NormalizeSqlForCaching / BuildCacheKey
▼
Services/QueryPlanCache.cs ← ConcurrentDictionary<string, CacheEntry>
│ (+ LRU eviction, capacity configurable)
▼
CacheEntry { CachedPlan, CompiledPlan, OptimizedPlan, OptimizedCost, CachedAtUtc, AccessCount }
Configuration (DatabaseConfig, defaults shown):
var config = new DatabaseConfig
{
EnableCompiledPlanCache = true, // default
CompiledPlanCacheCapacity = 2048, // default; LRU eviction past this
NormalizeSqlForPlanCache = true, // collapse whitespace so equal queries share an entry
};Cache key = normalised SQL + parameter shape + command type (SqlCommandType), so an INSERT and
a SELECT over the same text never share an entry.
Diagnostics and clearing are internal — Database.GetPlanCacheStats() ((Hits, Misses, HitRate, Count)) and Database.ClearPlanCache() are visible to the SharpCoreDB.Tests /
SharpCoreDB.Benchmarks assemblies through InternalsVisibleTo, but not callable from application
code. Applications that host their own Services.QueryPlanCache instance can use its public
GetStatistics() / Clear().
The compiled path's planner is deliberately light: BuildLogicalPlan reads the AST's shape (a few
flags and EstimatedRows from CostEstimator.EstimateScanCost), EstimateCost accumulates a heuristic
cost for each recognised clause, and BuildPhysicalSteps emits the step array. There is no
combinatorial search — no join-order enumeration and no subquery rewrite — so planning here is
O(clauses).
Earlier revisions of this page published an "Execution Impact" table (2-5× pushdown, 5-20× join reordering, 50-1000× combined). Those multipliers were never measured against this code path and have been removed. For measured, reproducible numbers — including the planner's contribution — use the benchmark harness and the reports indexed in
docs/benchmarks/BENCHMARK_METHOD.mdanddocs/manual/performance.md.
var estimator = new CostEstimator(tableStatistics); // IReadOnlyDictionary<string, TableStatistics>
var optimizer = new QueryOptimizer(estimator);
PhysicalPlan plan = optimizer.Optimize(selectNode);
double cost = plan.EstimatedCost;
LogicalPlan logical = plan.Logical; // shape flags + EstimatedRows
foreach (var step in plan.Steps)
{
Console.WriteLine($"{step.Type}: {step.TableName ?? "N/A"}");
}var estimator = new CostEstimator(statistics);
EstimatedCost scan = estimator.EstimateScanCost("orders");
EstimatedCost filtered = estimator.EstimateFilterCost(scan);
Console.WriteLine($"scan cost={scan.Cost} rows={scan.OutputRows} | " +
$"filter cost={filtered.Cost} rows={filtered.OutputRows}");Callers do not wire caching up. Database.ExecuteSQL / ExecuteQuery go through
Database.PlanCaching.cs, which normalises the SQL and asks Services.QueryPlanCache for an existing
entry before compiling a new plan. Configuration and the (internal) diagnostics are in §6.
PhysicalPlan.Steps describes the statement's clause order; it is not a second execution engine:
PhysicalStep(TableScan, "orders") → rows from the table
PhysicalStep(Filter, "id > 100") → predicate evaluation (compiled whereFilter / §3 helper)
PhysicalStep(Aggregate) → grouping and aggregation
PhysicalStep(Sort) → ORDER BY
PhysicalStep(Project) → projection / selected columns
Executors that exist under src/SharpCoreDB/Execution/: JoinExecutor (with
JoinExecutor.Diagnostics and JoinConditionEvaluator) for joins, AggregationOptimizer for
aggregates, and the subquery trio from §4. FilterExecutor and TableScanExecutor are not types in
this codebase — earlier revisions named them — and PhysicalStepType has no HashJoin member, because
joins are not a plan-step type here.
| Rule | Status on the engine's query path | Where |
|---|---|---|
| Cost-based plan shape | ✅ applied — heuristic cost per recognised clause | Optimization/QueryOptimizer.EstimateCost |
| Predicate pushdown (as a rewrite) | ❌ not applied — LogicalPlan.PredicatesPushed is hasWhere || hasHaving ("pushdown planned") |
Optimization/QueryOptimizer.BuildLogicalPlan |
Predicate evaluation helper (CanPushDown, most-selective-first ordering, SIMD-aware row filtering) |
✅ available, not wired into the compiled path | Planning/PredicatePushdown (static) |
| Join reordering | ❌ not applied — LogicalPlan.JoinsReordered = hasJoins ("placeholder flag") |
Optimization/QueryOptimizer.BuildLogicalPlan |
| Join-order algorithms | ✅ exist on unused components | Planning.QueryOptimizer.OptimizeJoinOrder, Query.CostBasedOptimizer.OptimizeJoinOrder |
| Subquery → semi/anti-join conversion | ❌ does not exist (SemiJoin/AntiJoin absent from the source) |
— |
| Subquery result caching with per-table invalidation | ✅ applied | Execution/SubqueryCache |
| Compiled-plan caching (normalised SQL + parameter shape + command type) | ✅ applied, on by default | Database/Caching/Database.PlanCaching.cs |
| Hash join selection | ❌ disabled — threshold is int.MaxValue ("until key hashing is implemented") |
Execution/JoinExecutor.cs |
What is not on the path yet, in rough priority order:
- Join reordering — the flags exist; the search does not.
OptimizeJoinOrderimplementations are already written and tested on two unused components. - Subquery→join rewriting — needed before
SubqueriesEliminatedcan mean anything beyond "the source is not a subquery". - Hash joins — requires key hashing in
JoinExecutor; today's joins are nested-loop. - Index-aware plan choice on the compiled path (
Query/CostBasedOptimizer.ChooseBestIndexexists, unused). - Deeper selectivity statistics, partition pruning, and parallel operator execution — none implemented.
- Plan shape:
LogicalPlanandPhysicalPlanarereadonly structs, andPhysicalStepis a small struct array — the plan path is allocation-light, but "zero allocation" would overstate it: eachOptimizecall builds aPhysicalStep[]. - Not a search engine: no Selinger-style dynamic programming, no plan enumeration beyond the single costed plan this class builds.
- Honesty over multipliers: this page deliberately carries no per-rule speedup table; the numbers
that are published for this engine are measured end-to-end and live in
docs/benchmarks/anddocs/manual/performance.md. - .NET 11 / C# 15: the implementation uses
requiredmembers,initproperties and pattern matching, consistent with the rest of the codebase.