Skip to content

Commit d53f97c

Browse files
authored
Merge pull request #2741 from mabel-dev/#2240
Initial cost-based optimization #2240
2 parents c50ebb5 + c343ff4 commit d53f97c

13 files changed

Lines changed: 225 additions & 238 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,4 @@ hits_split/*.parquet
184184
clickbench/**
185185
.github/environment/splunk/docker-compose.yml
186186
testdata/iceberg/**
187+
third_party/tktech/simdjson/csimdjson.cpp

opteryx/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__build__ = 1440
1+
__build__ = 1444
22

33
# Licensed under the Apache License, Version 2.0 (the "License");
44
# you may not use this file except in compliance with the License.

opteryx/compiled/structures/relation_statistics.pyx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,3 +224,6 @@ cdef class RelationStatistics:
224224
read_map(buf, &offset, inst.upper_bounds)
225225
read_map(buf, &offset, inst.cardinality_estimate)
226226
return inst
227+
228+
def __deepcopy__(self, memo):
229+
return self

opteryx/connectors/sql_connector.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@
2525
from orso.types import PYTHON_TO_ORSO_MAP
2626
from orso.types import OrsoTypes
2727

28+
from opteryx.compiled.structures.relation_statistics import RelationStatistics
2829
from opteryx.config import OPTERYX_DEBUG
2930
from opteryx.connectors.base.base_connector import DEFAULT_MORSEL_SIZE
3031
from opteryx.connectors.base.base_connector import INITIAL_CHUNK_SIZE
3132
from opteryx.connectors.base.base_connector import MIN_CHUNK_SIZE
3233
from opteryx.connectors.base.base_connector import BaseConnector
3334
from opteryx.connectors.capabilities import LimitPushable
3435
from opteryx.connectors.capabilities import PredicatePushable
36+
from opteryx.connectors.capabilities import Statistics
3537
from opteryx.exceptions import DatasetReadError
3638
from opteryx.exceptions import MissingDependencyError
3739
from opteryx.exceptions import UnmetRequirementError
@@ -53,7 +55,7 @@ def _handle_operand(operand: Node, parameters: dict) -> Tuple[Any, dict]:
5355
return f":{name}", parameters
5456

5557

56-
class SqlConnector(BaseConnector, LimitPushable, PredicatePushable):
58+
class SqlConnector(BaseConnector, LimitPushable, PredicatePushable, Statistics):
5759
__mode__ = "Sql"
5860
__type__ = "SQL"
5961

@@ -91,6 +93,7 @@ def __init__(self, *args, connection: str = None, engine=None, **kwargs):
9193
BaseConnector.__init__(self, **kwargs)
9294
LimitPushable.__init__(self, **kwargs)
9395
PredicatePushable.__init__(self, **kwargs)
96+
Statistics.__init__(self, **kwargs)
9497

9598
try:
9699
from sqlalchemy import MetaData
@@ -224,6 +227,67 @@ def read_dataset( # type:ignore
224227

225228
# DEBUG: print(f"time spent converting: {convert_time/1e9}s")
226229

230+
def collect_relation_stats(self) -> RelationStatistics:
231+
from sqlalchemy import inspect
232+
from sqlalchemy.sql import text
233+
234+
stats = RelationStatistics()
235+
dialect = self._engine.dialect.name.lower()
236+
237+
if dialect == "postgresql":
238+
row_est = self._engine.execute(
239+
text("SELECT reltuples::BIGINT FROM pg_class WHERE relname = :t"),
240+
{"t": self.dataset},
241+
).scalar()
242+
stats.record_count_estimate = int(row_est)
243+
244+
pg_stats = self._engine.execute(
245+
text("""
246+
SELECT attname, n_distinct, null_frac, histogram_bounds
247+
FROM pg_stats
248+
WHERE tablename = :t
249+
"""),
250+
{"t": self.dataset},
251+
).fetchall()
252+
253+
for row in pg_stats:
254+
col = row["attname"]
255+
stats.cardinality_estimate[col] = (
256+
int(row["n_distinct"]) if row["n_distinct"] > 0 else 0
257+
)
258+
stats.null_count[col] = int(row["null_frac"] * row_est)
259+
bounds = row["histogram_bounds"]
260+
if bounds and isinstance(bounds, list) and len(bounds) >= 2:
261+
stats.lower_bounds[col] = bounds[0]
262+
stats.upper_bounds[col] = bounds[-1]
263+
264+
elif dialect in {"duckdb", "sqlite", "mysql"}:
265+
# fallback: query full stats for small/embedded engines
266+
columns = inspect(self._engine).get_columns(self.dataset)
267+
numeric_cols = [
268+
col["name"]
269+
for col in columns
270+
if str(col["type"]).lower()
271+
in {"integer", "bigint", "float", "real", "numeric", "double"}
272+
]
273+
274+
# Build dynamic query
275+
parts = ["COUNT(*) AS count"]
276+
for col in numeric_cols:
277+
parts.extend([f"MIN({col}) AS min_{col}", f"MAX({col}) AS max_{col}"])
278+
q = f"SELECT {', '.join(parts)} FROM {self.dataset}"
279+
with self._engine.connect() as conn:
280+
# DEBUG: print("READ STATS\n", str(q))
281+
result = conn.execute(text(q)).fetchone()._asdict()
282+
283+
stats.record_count = result["count"]
284+
stats.record_count_estimate = result["count"]
285+
for col in numeric_cols:
286+
stats.lower_bounds[col] = result[f"min_{col}"]
287+
stats.upper_bounds[col] = result[f"max_{col}"]
288+
289+
return stats
290+
227291
def get_dataset_schema(self) -> RelationSchema:
228292
from sqlalchemy import Table
229293

@@ -289,4 +353,6 @@ def get_dataset_schema(self) -> RelationSchema:
289353
except Exception as err:
290354
raise DatasetReadError(f"Unable to read dataset '{self.dataset}'.") from err
291355

356+
self.schema.relation_statistics = self.collect_relation_stats()
357+
292358
return self.schema

opteryx/planner/optimizer/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,12 @@ def __init__(self, statistics: QueryStatistics):
8080
PredicateRewriteStrategy(statistics),
8181
PredicatePushdownStrategy(statistics),
8282
ProjectionPushdownStrategy(statistics),
83+
JoinRewriteStrategy(statistics),
8384
JoinOrderingStrategy(statistics),
8485
DistinctPushdownStrategy(statistics),
8586
OperatorFusionStrategy(statistics),
8687
LimitPushdownStrategy(statistics),
87-
PredicateFlatteningStrategy(statistics),
88+
PredicateOrderingStrategy(statistics),
8889
RedundantOperationsStrategy(statistics),
8990
ConstantFoldingStrategy(statistics),
9091
]

opteryx/planner/optimizer/bench/cost_based_optimizer/__init__.py

Whitespace-only changes.

opteryx/planner/optimizer/bench/cost_based_optimizer/cost_model.py

Lines changed: 0 additions & 21 deletions
This file was deleted.

opteryx/planner/optimizer/bench/cost_based_optimizer/predicate_ordering_brute.py

Lines changed: 0 additions & 48 deletions
This file was deleted.

opteryx/planner/optimizer/bench/cost_based_optimizer/predicate_ordering_genetic.py

Lines changed: 0 additions & 157 deletions
This file was deleted.

opteryx/planner/optimizer/strategies/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
from .correlated_filters import CorrelatedFiltersStrategy
44
from .distinct_pushdown import DistinctPushdownStrategy
55
from .join_ordering import JoinOrderingStrategy
6+
from .join_rewriter import JoinRewriteStrategy
67
from .limit_pushdown import LimitPushdownStrategy
78
from .operator_fusion import OperatorFusionStrategy
8-
from .predicate_flatten import PredicateFlatteningStrategy
9+
from .predicate_ordering import PredicateOrderingStrategy
910
from .predicate_pushdown import PredicatePushdownStrategy
1011
from .predicate_rewriter import PredicateRewriteStrategy
1112
from .projection_pushdown import ProjectionPushdownStrategy
@@ -18,9 +19,10 @@
1819
"CorrelatedFiltersStrategy",
1920
"DistinctPushdownStrategy",
2021
"JoinOrderingStrategy",
22+
"JoinRewriteStrategy",
2123
"LimitPushdownStrategy",
2224
"OperatorFusionStrategy",
23-
"PredicateFlatteningStrategy",
25+
"PredicateOrderingStrategy",
2426
"PredicatePushdownStrategy",
2527
"PredicateRewriteStrategy",
2628
"ProjectionPushdownStrategy",

0 commit comments

Comments
 (0)