优化器直方图

This commit is contained in:
LIm1tZz7 committed 2026-10-03 21:03:17 +08:00
1 parent 7d96933e32
commit d59e65b639
17 files changed
+353 -49

No files matched your search

+1 -1
View File
@@ -28,5 +28,5 @@
- 改动说明只在交流中记录。
## 当前重点
- 主线已完成:高级优化器 → 并行查询 → M1 会话隔离 → M2 快照事务 → M3 WAL 与崩溃恢复 → M4 表结构进 WAL → M5 系统表统一命名 → M6 检查点按 LSN 部分截断 → M7 事务写写冲突检测 → M8 事务保存点 → M9 角色继承 → M10 转授权 → M11 FROM 派生表与投影别名 → M12 `WITH` / CTE → M13 比较运算符补全 → M14 ANY / ALL 量词比较。
- 主线已完成:高级优化器 → 并行查询 → M1 会话隔离 → M2 快照事务 → M3 WAL 与崩溃恢复 → M4 表结构进 WAL → M5 系统表统一命名 → M6 检查点按 LSN 部分截断 → M7 事务写写冲突检测 → M8 事务保存点 → M9 角色继承 → M10 转授权 → M11 FROM 派生表与投影别名 → M12 `WITH` / CTE → M13 比较运算符补全 → M14 ANY / ALL 量词比较 → M15 优化器直方图。
- 进度与计数见 `agent_credientials/project-plan.txt`,限制与风险见 `agent_credientials/known-issues.txt`。
@@ -1,8 +1,8 @@
module ChuSQL.Core.Engine.Algebra.Cost (tableMetaOf, probeCost, pointRows, rangeRows, indexCheaper) where
module ChuSQL.Core.Engine.Algebra.Cost (tableMetaOf, probeCost, pointRows, rangeRows, selectivityOf, indexCheaper) where
import ChuSQL.Core.Model
-- 统计与成本估算:行数、列级不同值数与索引探路页数。
-- 统计与成本估算:行数、不同值数、列直方图与索引探路页数。
-- | 表统计,拿不到就是 Nothing
tableMetaOf :: Database -> String -> Maybe TableMeta
@@ -20,9 +20,42 @@ pointRows m col = case [d | (c, d, _) <- metaDistinct m, c == col, d > 1] of
where
rows = max 0 (metaRowCount m)
-- | 范围扫描的估算命中行数:没有直方图,按四分之一算
rangeRows :: TableMeta -> Int
rangeRows m = max 0 (metaRowCount m) `div` 4
-- | 范围扫描的估算命中行数:有直方图按桶算,没有按四分之一
rangeRows :: TableMeta -> String -> Maybe (Value, Bool) -> Maybe (Value, Bool) -> Int
rangeRows m col lo hi = case histogramFor m col of
Just h -> estimateRows h lo hi
Nothing -> max 0 (metaRowCount m) `div` 4
-- | 直方图估算区间命中行数:按桶算交集占比
estimateRows :: Histogram -> Maybe (Value, Bool) -> Maybe (Value, Bool) -> Int
estimateRows h lo hi = max 0 (min total (round (sum (zipWith overlap [0 ..] (histBuckets h)))))
where
total = sum (histBuckets h)
width = if histHigh h > histLow h then (histHigh h - histLow h) / fromIntegral (length (histBuckets h)) else 0
lower = maybe (-1 / 0) id (boundOf lo)
upper = maybe (1 / 0) id (boundOf hi)
overlap i count
| width <= 0 = if lower <= histLow h && histLow h <= upper then fromIntegral count else 0
| otherwise = fromIntegral count * hit / width
where
start = histLow h + width * fromIntegral i
hit = max 0 (min (start + width) upper - max start lower)
-- | 区间选择率:直方图命中的比例;没有直方图给 Nothing
selectivityOf :: TableMeta -> String -> Maybe (Value, Bool) -> Maybe (Value, Bool) -> Maybe Double
selectivityOf m col lo hi = case histogramFor m col of
Nothing -> Nothing
Just h
| total <= 0 -> Nothing
| otherwise -> Just (fromIntegral (estimateRows h lo hi) / fromIntegral total)
where
total = sum (histBuckets h)
-- | 边界值转成参与估值的小数;非数值给 Nothing
boundOf :: Maybe (Value, Bool) -> Maybe Double
boundOf (Just (VInt n, _)) = Just (fromIntegral n)
boundOf (Just (VFloat d, _)) = Just d
boundOf _ = Nothing
-- | 有索引且探路加命中比全表扫便宜
indexCheaper :: TableMeta -> String -> Int -> Bool
@@ -1,6 +1,6 @@
module ChuSQL.Core.Engine.Algebra.JoinOrder (reorderJoins) where
import ChuSQL.Core.Engine.Algebra.Cost (pointRows, rangeRows, tableMetaOf)
import ChuSQL.Core.Engine.Algebra.Cost (pointRows, rangeRows, selectivityOf, tableMetaOf)
import ChuSQL.Core.Engine.Algebra.Expr (colsInExpr, hasDivision)
import ChuSQL.Core.Engine.Algebra.Op (RelOp (..), relOpCols)
import ChuSQL.Core.Engine.Syntax.AST (Expr (..), JoinKind (..))
@@ -45,7 +45,7 @@ leafRows :: Database -> RelOp -> Maybe Int
leafRows db op = case op of
Scan _ t _ -> rowCountOf db t
Lookup _ t c _ -> pointRows <$> tableMetaOf db t <*> pure c
Range _ t _ _ _ -> rangeRows <$> tableMetaOf db t
Range _ t c lo hi -> (\m -> rangeRows m c lo hi) <$> tableMetaOf db t
Filter p x -> scaleRows db p x <$> leafRows db x
Project _ x -> leafRows db x
_ -> Nothing
@@ -54,12 +54,20 @@ leafRows db op = case op of
rowCountOf :: Database -> String -> Maybe Int
rowCountOf db t = metaRowCount <$> tableMetaOf db t
-- | 过滤之后的大致行数:等值看不同值数,其他按四分之一
-- | 行数估算:等值看不同值数,区间看直方图,其余四分之一
scaleRows :: Database -> Expr -> RelOp -> Int -> Int
scaleRows db p x rows = case p of
Eq (Col k) _ -> case distinctOf db x k of
Just d | d > 1 -> max 1 (rows `div` d)
_ -> quarter
Gt (Col k) (LitInt v) -> pick k (Just (VInt v, False)) Nothing
Gt (Col k) (LitStr v) -> pick k (Just (VStr v, False)) Nothing
GtE (Col k) (LitInt v) -> pick k (Just (VInt v, True)) Nothing
GtE (Col k) (LitStr v) -> pick k (Just (VStr v, True)) Nothing
Lt (Col k) (LitInt v) -> pick k Nothing (Just (VInt v, False))
Lt (Col k) (LitStr v) -> pick k Nothing (Just (VStr v, False))
LtE (Col k) (LitInt v) -> pick k Nothing (Just (VInt v, True))
LtE (Col k) (LitStr v) -> pick k Nothing (Just (VStr v, True))
Gt _ _ -> quarter
Lt _ _ -> quarter
GtE _ _ -> quarter
@@ -70,6 +78,11 @@ scaleRows db p x rows = case p of
_ -> quarter
where
quarter = max 1 (rows `div` 4)
pick k lo hi = case leafSource x of
Just (alias, t)
| Just m <- tableMetaOf db t
, Just s <- selectivityOf m (unqualify alias k) lo hi -> max 1 (round (fromIntegral rows * s))
_ -> quarter
-- | 叶子对应的别名与表名
leafSource :: RelOp -> Maybe (Maybe String, String)
@@ -159,10 +159,10 @@ worthLookup db t col = case tableMetaOf db t of
Just m -> indexCheaper m col (pointRows m col)
-- | 范围扫描值不值得改写;没有统计就按老规矩改
worthRange :: Database -> String -> String -> Bool
worthRange db t col = case tableMetaOf db t of
worthRange :: Database -> String -> String -> Maybe (Value, Bool) -> Maybe (Value, Bool) -> Bool
worthRange db t col lo hi = case tableMetaOf db t of
Nothing -> True
Just m -> indexCheaper m col (rangeRows m)
Just m -> indexCheaper m col (rangeRows m col lo hi)
-- | 单节点重写
rewriteNode :: Database -> RelOp -> RelOp
@@ -171,45 +171,53 @@ rewriteNode db (Filter (Eq (Col k) (LitInt v)) (Scan mAlias t Nothing))
rewriteNode db (Filter (Eq (Col k) (LitStr v)) (Scan mAlias t Nothing))
| worthLookup db t (unqualify mAlias k) = Lookup mAlias t (unqualify mAlias k) (VStr v)
rewriteNode db (Filter (And (Gt (Col k1) (LitInt lo)) (Lt (Col k2) (LitInt hi))) (Scan mAlias t Nothing))
| k1 == k2 && worthRange db t (unqualify mAlias k1) =
| k1 == k2 && worthRange db t (unqualify mAlias k1) (Just (VInt lo, False)) (Just (VInt hi, False)) =
Range mAlias t (unqualify mAlias k1) (Just (VInt lo, False)) (Just (VInt hi, False))
rewriteNode db (Filter (And (Gt (Col k1) (LitStr lo)) (Lt (Col k2) (LitStr hi))) (Scan mAlias t Nothing))
| k1 == k2 && worthRange db t (unqualify mAlias k1) =
| k1 == k2 && worthRange db t (unqualify mAlias k1) (Just (VStr lo, False)) (Just (VStr hi, False)) =
Range mAlias t (unqualify mAlias k1) (Just (VStr lo, False)) (Just (VStr hi, False))
rewriteNode db (Filter (Gt (Col k) (LitInt v)) (Scan mAlias t Nothing))
| worthRange db t (unqualify mAlias k) = Range mAlias t (unqualify mAlias k) (Just (VInt v, False)) Nothing
| worthRange db t (unqualify mAlias k) (Just (VInt v, False)) Nothing =
Range mAlias t (unqualify mAlias k) (Just (VInt v, False)) Nothing
rewriteNode db (Filter (Gt (Col k) (LitStr v)) (Scan mAlias t Nothing))
| worthRange db t (unqualify mAlias k) = Range mAlias t (unqualify mAlias k) (Just (VStr v, False)) Nothing
| worthRange db t (unqualify mAlias k) (Just (VStr v, False)) Nothing =
Range mAlias t (unqualify mAlias k) (Just (VStr v, False)) Nothing
rewriteNode db (Filter (Lt (Col k) (LitInt v)) (Scan mAlias t Nothing))
| worthRange db t (unqualify mAlias k) = Range mAlias t (unqualify mAlias k) Nothing (Just (VInt v, False))
| worthRange db t (unqualify mAlias k) Nothing (Just (VInt v, False)) =
Range mAlias t (unqualify mAlias k) Nothing (Just (VInt v, False))
rewriteNode db (Filter (Lt (Col k) (LitStr v)) (Scan mAlias t Nothing))
| worthRange db t (unqualify mAlias k) = Range mAlias t (unqualify mAlias k) Nothing (Just (VStr v, False))
| worthRange db t (unqualify mAlias k) Nothing (Just (VStr v, False)) =
Range mAlias t (unqualify mAlias k) Nothing (Just (VStr v, False))
rewriteNode db (Filter (And (GtE (Col k1) (LitInt lo)) (LtE (Col k2) (LitInt hi))) (Scan mAlias t Nothing))
| k1 == k2 && worthRange db t (unqualify mAlias k1) =
| k1 == k2 && worthRange db t (unqualify mAlias k1) (Just (VInt lo, True)) (Just (VInt hi, True)) =
Range mAlias t (unqualify mAlias k1) (Just (VInt lo, True)) (Just (VInt hi, True))
rewriteNode db (Filter (And (GtE (Col k1) (LitInt lo)) (Lt (Col k2) (LitInt hi))) (Scan mAlias t Nothing))
| k1 == k2 && worthRange db t (unqualify mAlias k1) =
| k1 == k2 && worthRange db t (unqualify mAlias k1) (Just (VInt lo, True)) (Just (VInt hi, False)) =
Range mAlias t (unqualify mAlias k1) (Just (VInt lo, True)) (Just (VInt hi, False))
rewriteNode db (Filter (And (Gt (Col k1) (LitInt lo)) (LtE (Col k2) (LitInt hi))) (Scan mAlias t Nothing))
| k1 == k2 && worthRange db t (unqualify mAlias k1) =
| k1 == k2 && worthRange db t (unqualify mAlias k1) (Just (VInt lo, False)) (Just (VInt hi, True)) =
Range mAlias t (unqualify mAlias k1) (Just (VInt lo, False)) (Just (VInt hi, True))
rewriteNode db (Filter (And (GtE (Col k1) (LitStr lo)) (LtE (Col k2) (LitStr hi))) (Scan mAlias t Nothing))
| k1 == k2 && worthRange db t (unqualify mAlias k1) =
| k1 == k2 && worthRange db t (unqualify mAlias k1) (Just (VStr lo, True)) (Just (VStr hi, True)) =
Range mAlias t (unqualify mAlias k1) (Just (VStr lo, True)) (Just (VStr hi, True))
rewriteNode db (Filter (And (GtE (Col k1) (LitStr lo)) (Lt (Col k2) (LitStr hi))) (Scan mAlias t Nothing))
| k1 == k2 && worthRange db t (unqualify mAlias k1) =
| k1 == k2 && worthRange db t (unqualify mAlias k1) (Just (VStr lo, True)) (Just (VStr hi, False)) =
Range mAlias t (unqualify mAlias k1) (Just (VStr lo, True)) (Just (VStr hi, False))
rewriteNode db (Filter (And (Gt (Col k1) (LitStr lo)) (LtE (Col k2) (LitStr hi))) (Scan mAlias t Nothing))
| k1 == k2 && worthRange db t (unqualify mAlias k1) =
| k1 == k2 && worthRange db t (unqualify mAlias k1) (Just (VStr lo, False)) (Just (VStr hi, True)) =
Range mAlias t (unqualify mAlias k1) (Just (VStr lo, False)) (Just (VStr hi, True))
rewriteNode db (Filter (GtE (Col k) (LitInt v)) (Scan mAlias t Nothing))
| worthRange db t (unqualify mAlias k) = Range mAlias t (unqualify mAlias k) (Just (VInt v, True)) Nothing
| worthRange db t (unqualify mAlias k) (Just (VInt v, True)) Nothing =
Range mAlias t (unqualify mAlias k) (Just (VInt v, True)) Nothing
rewriteNode db (Filter (GtE (Col k) (LitStr v)) (Scan mAlias t Nothing))
| worthRange db t (unqualify mAlias k) = Range mAlias t (unqualify mAlias k) (Just (VStr v, True)) Nothing
| worthRange db t (unqualify mAlias k) (Just (VStr v, True)) Nothing =
Range mAlias t (unqualify mAlias k) (Just (VStr v, True)) Nothing
rewriteNode db (Filter (LtE (Col k) (LitInt v)) (Scan mAlias t Nothing))
| worthRange db t (unqualify mAlias k) = Range mAlias t (unqualify mAlias k) Nothing (Just (VInt v, True))
| worthRange db t (unqualify mAlias k) Nothing (Just (VInt v, True)) =
Range mAlias t (unqualify mAlias k) Nothing (Just (VInt v, True))
rewriteNode db (Filter (LtE (Col k) (LitStr v)) (Scan mAlias t Nothing))
| worthRange db t (unqualify mAlias k) = Range mAlias t (unqualify mAlias k) Nothing (Just (VStr v, True))
| worthRange db t (unqualify mAlias k) Nothing (Just (VStr v, True)) =
Range mAlias t (unqualify mAlias k) Nothing (Just (VStr v, True))
rewriteNode db (Filter p (Join kind l r c))
| not (hasDivision p || planHasDivision (Join kind l r c)) =
case kind of
@@ -401,7 +401,7 @@ instance MonadStorage IPCStorage where
-- | 线上表信息转引擎侧统计
metaOf :: TableInfo -> TableMeta
metaOf info = TableMeta (tiRows info) (tiStats info) (tiIndexes info)
metaOf info = TableMeta (tiRows info) (tiStats info) (tiHistograms info) (tiIndexes info)
-- | 没设库名就照单全收;设了库名只收这个库的表,键名去掉库名前缀
visibleTables :: Maybe String -> TableInfo -> [(String, Table)]
+45 -6
View File
@@ -2,6 +2,7 @@
module Main where
import ChuSQL.Core.Engine.Algebra.Cost (rangeRows, selectivityOf)
import ChuSQL.Core.Engine.Algebra.Eval (evalRelOp, evalRelOpM)
import ChuSQL.Core.Engine.Algebra.Expr (colsInExpr, evalExpr)
import ChuSQL.Core.Engine.Algebra.Op (RelOp (..), renderPlan)
@@ -71,7 +72,7 @@ chainA =
{ tableName = "a"
, tableCols = [("id", TInt), ("name", TStr)]
, tableRows = [[("id", VInt n), ("name", VStr ("a" ++ show n))] | n <- [1 .. 6]]
, tableMeta = Just (TableMeta 6 [("id", 6, False)] ["id"])
, tableMeta = Just (TableMeta 6 [("id", 6, False)] [] ["id"])
}
-- | 连接链测试表 b:4 行,a_id 各不相同
@@ -81,7 +82,7 @@ chainB =
{ tableName = "b"
, tableCols = [("id", TInt), ("a_id", TInt)]
, tableRows = [[("id", VInt n), ("a_id", VInt n)] | n <- [1 .. 4]]
, tableMeta = Just (TableMeta 4 [("id", 4, False), ("a_id", 4, False)] ["id"])
, tableMeta = Just (TableMeta 4 [("id", 4, False), ("a_id", 4, False)] [] ["id"])
}
-- | 连接链测试表 c:2 行,b_id 各不相同
@@ -91,7 +92,7 @@ chainC =
{ tableName = "c"
, tableCols = [("id", TInt), ("b_id", TInt)]
, tableRows = [[("id", VInt n), ("b_id", VInt n)] | n <- [1, 2]]
, tableMeta = Just (TableMeta 2 [("id", 2, False), ("b_id", 2, False)] ["id"])
, tableMeta = Just (TableMeta 2 [("id", 2, False), ("b_id", 2, False)] [] ["id"])
}
-- | 三张表组成的连接链测试库
@@ -551,7 +552,7 @@ main = hspec $ do
executeOperator OpNe [VInt 1, VFloat 1.0] `shouldBe` Right (VBool False)
it "rewrites >= and <= into an inclusive index range" $ do
let stat rows distinct indexes = TableMeta rows [(c, d, False) | (c, d) <- distinct] indexes
let stat rows distinct indexes = TableMeta rows [(c, d, False) | (c, d) <- distinct] [] indexes
dbWith m = [("users", Table "users" [("id", TInt), ("name", TStr)] [] (Just m))]
indexed = dbWith (stat 10000 [("id", 10000)] ["id"])
low = Filter (GtE (Col "id") (LitInt 2)) (Scan Nothing "users" Nothing)
@@ -1509,6 +1510,44 @@ main = hspec $ do
[ [("name", VStr "Carol")]
]
describe "ChuSQL.Core.Engine.Algebra.Cost (直方图)" $ do
it "builds a 16-bucket histogram from the numeric values" $ do
let hist = histogramOf [VInt n | n <- [0 .. 99]]
fmap histLow hist `shouldBe` Just 0
fmap histHigh hist `shouldBe` Just 99
fmap (length . histBuckets) hist `shouldBe` Just 16
fmap (sum . histBuckets) hist `shouldBe` Just 100
-- 非数值与 NULL 不参与;只有一个值都落第 0 桶
histogramOf [VStr "a", VNull] `shouldBe` Nothing
histogramOf [VInt 5, VNull, VStr "a"] `shouldBe` Just (Histogram 5 5 (1 : replicate 15 0))
it "estimates a range from the histogram buckets" $ do
let hist = Histogram 0 1600 (replicate 16 100)
meta = TableMeta 1600 [] [("id", hist)] ["id"]
plain = TableMeta 1600 [] [] []
rangeRows meta "id" (Just (VInt 0, True)) (Just (VInt 100, True)) `shouldBe` 100
rangeRows meta "id" Nothing Nothing `shouldBe` 1600
selectivityOf meta "id" (Just (VInt 0, True)) (Just (VInt 100, True)) `shouldBe` Just 0.0625
-- 没有直方图退回四分之一
rangeRows plain "id" Nothing Nothing `shouldBe` 400
selectivityOf plain "id" Nothing Nothing `shouldBe` Nothing
it "uses the histogram to reject a range that would scan most rows" $ do
let buckets = replicate 8 63 ++ replicate 8 62
withHist = TableMeta 1000 [("id", 1000, False)] [("id", Histogram 0 1600 buckets)] ["id"]
plain = TableMeta 1000 [("id", 1000, False)] [] ["id"]
dbWith m = [("users", Table "users" [("id", TInt), ("name", TStr)] [] (Just m))]
narrow = Filter (And (GtE (Col "id") (LitInt 10)) (LtE (Col "id") (LitInt 20))) (Scan Nothing "users" Nothing)
wide = Filter (Gt (Col "id") (LitInt 0)) (Scan Nothing "users" Nothing)
-- 直方图说这个窄范围只命中十几行:改写
optimize (dbWith withHist) narrow
`shouldBe` Range Nothing "users" "id" (Just (VInt 10, True)) (Just (VInt 20, True))
-- 直方图说这条范围覆盖全表:索引不比全表扫便宜,不改写
optimize (dbWith withHist) wide `shouldBe` wide
-- 没有直方图按四分之一算,仍然改写
optimize (dbWith plain) wide
`shouldBe` Range Nothing "users" "id" (Just (VInt 0, False)) Nothing
describe "ChuSQL.Core.Engine.Algebra.Optimize" $ do
it "keeps single-table WHERE results identical" $ do
sameResultAsUnoptimized "SELECT name FROM users WHERE age > 18"
@@ -1559,7 +1598,7 @@ main = hspec $ do
`shouldBe` Right (Just (LitBool False))
it "picks the index only when the statistics make it cheaper" $ do
let stat rows distinct indexes = TableMeta rows [(c, d, False) | (c, d) <- distinct] indexes
let stat rows distinct indexes = TableMeta rows [(c, d, False) | (c, d) <- distinct] [] indexes
dbWith m = [("users", Table "users" [("id", TInt), ("name", TStr)] [] (Just m))]
unknown = [("users", Table "users" [("id", TInt), ("name", TStr)] [] Nothing)]
point = Filter (Eq (Col "id") (LitInt 1)) (Scan Nothing "users" Nothing)
@@ -2689,7 +2728,7 @@ rowShards width rows
-- | 声明 1000 行的大表
bigTable :: Table
bigTable = users {tableName = "big", tableMeta = Just (TableMeta 1000 [("id", 1000, False)] ["id"])}
bigTable = users {tableName = "big", tableMeta = Just (TableMeta 1000 [("id", 1000, False)] [] ["id"])}
-- | 只装大表的库
bigDB :: Database
+44 -2
View File
@@ -10,8 +10,11 @@ module ChuSQL.Core.Model
, Value (..)
, Row
, TableMeta (..)
, Histogram (..)
, Table (..)
, Database
, histogramOf
, histogramFor
, colNames
, colType
, lookupTable
@@ -42,7 +45,7 @@ module ChuSQL.Core.Model
import Data.Char (isDigit, isSpace, toLower)
import Data.Hashable (Hashable (..))
import Data.List (isSuffixOf, stripPrefix)
import Data.Maybe (fromMaybe)
import Data.Maybe (fromMaybe, mapMaybe)
-- 数据模型:列类型、列约束、值、行、表、数据库,外加查表小工具。
@@ -441,10 +444,19 @@ instance Hashable Value where
-- | 一行数据
type Row = [(String, Value)]
-- | 一张表的统计:行数、各列不同值数(capped 表示只数到上限)、有索引的列
-- | 一列的等宽直方图:区间下界、上界与每桶计数
data Histogram = Histogram
{ histLow :: Double
, histHigh :: Double
, histBuckets :: [Int]
}
deriving (Show, Eq)
-- | 一张表的统计:行数、各列不同值数、列直方图与索引
data TableMeta = TableMeta
{ metaRowCount :: Int
, metaDistinct :: [(String, Int, Bool)]
, metaHistograms :: [(String, Histogram)]
, metaIndexes :: [String]
}
deriving (Show)
@@ -461,6 +473,36 @@ data Table = Table
-- | 库:库名到表的映射
type Database = [(String, Table)]
-- | 这一列的直方图
histogramFor :: TableMeta -> String -> Maybe Histogram
histogramFor m col = lookup col (metaHistograms m)
-- | 数值列按 16 个等宽桶统计,无数值给 Nothing
histogramOf :: [Value] -> Maybe Histogram
histogramOf values = case mapMaybe numericOf values of
[] -> Nothing
vs -> let lo = minimum vs; hi = maximum vs in Just (Histogram lo hi (bucketCounts lo hi vs))
-- | 值转成用于分桶的数;非数值给 Nothing
numericOf :: Value -> Maybe Double
numericOf (VInt n) = Just (fromIntegral n)
numericOf (VFloat d) = Just d
numericOf _ = Nothing
-- | 每个桶里落了多少个值
bucketCounts :: Double -> Double -> [Double] -> [Int]
bucketCounts lo hi vs = [length [v | v <- vs, bucketIndexOf lo hi v == i] | i <- [0 .. bucketTotal - 1]]
-- | 值落在第几个桶;区间退化时都算第 0 桶
bucketIndexOf :: Double -> Double -> Double -> Int
bucketIndexOf lo hi v
| hi <= lo = 0
| otherwise = max 0 (min (bucketTotal - 1) (floor ((v - lo) / (hi - lo) * fromIntegral bucketTotal) :: Int))
-- | 直方图的桶数
bucketTotal :: Int
bucketTotal = 16
-- | 取表的列名
colNames :: Table -> [String]
colNames = map fst . tableCols
+23 -7
View File
@@ -15,7 +15,7 @@ module ChuSQL.Core.Protocol (
queryResultJson,
) where
import ChuSQL.Core.Model (Row, Value (..))
import ChuSQL.Core.Model (Histogram (..), Row, Value (..))
import Data.Aeson (FromJSON (..), ToJSON (..), object, withObject, (.:), (.:?), (.!=), (.=))
import qualified Data.Aeson as A
import qualified Data.Aeson.Key as K
@@ -244,6 +244,7 @@ data TableInfo = TableInfo
, tiRows :: Int
, tiIndexes :: [String]
, tiStats :: [(String, Int, Bool)]
, tiHistograms :: [(String, Histogram)]
}
deriving (Show, Eq)
@@ -255,11 +256,21 @@ instance ToJSON TableInfo where
, "columns" .= tiColumns info
, "row_count" .= tiRows info
, "indexes" .= map (\name -> object ["column" .= name]) (tiIndexes info)
, "stats" .= map statJson (tiStats info)
, "stats" .= map (statJson (tiHistograms info)) (tiStats info)
]
where
statJson (name, distinct, capped) =
object ["name" .= name, "distinct" .= distinct, "capped" .= capped]
statJson hists (name, distinct, capped) =
object (statFields ++ histFields (lookup name hists))
where
statFields = ["name" .= name, "distinct" .= distinct, "capped" .= capped]
histFields Nothing = []
histFields (Just h) = ["lo" .= histLow h, "hi" .= histHigh h, "hist" .= histBuckets h]
-- | 线上直方图还原;区间或桶缺了就当作没有
histogramFromWire :: Maybe Double -> Maybe Double -> [Int] -> Maybe Histogram
histogramFromWire (Just low) (Just high) counts
| not (null counts) = Just (Histogram low high counts)
histogramFromWire _ _ _ = Nothing
-- | JSON 解回表信息
instance FromJSON TableInfo where
@@ -270,14 +281,19 @@ instance FromJSON TableInfo where
indexes <- o .:? "indexes" .!= []
stats <- o .:? "stats" .!= []
indexColumns <- mapM (\v -> withObject "IndexWire" (.: "column") v) (indexes :: [A.Value])
parsedStats <- mapM parseStat (stats :: [A.Value])
pure (TableInfo table columns count indexColumns parsedStats)
parsed <- mapM parseStat (stats :: [A.Value])
let parsedStats = [(name, distinct, capped) | (name, distinct, capped, _) <- parsed]
parsedHistograms = [(name, h) | (name, _, _, Just h) <- parsed]
pure (TableInfo table columns count indexColumns parsedStats parsedHistograms)
where
parseStat = withObject "ColumnStat" $ \o -> do
name <- o .: "name"
distinct <- o .: "distinct"
capped <- o .:? "capped" .!= False
pure (name, distinct, capped)
low <- o .:? "lo"
high <- o .:? "hi"
counts <- o .:? "hist" .!= []
pure (name, distinct, capped, histogramFromWire low high counts)
-- | 一条账号记录
data Account = Account
+76
View File
@@ -12,6 +12,7 @@ use crate::protocol::{Row, SchemaColumn};
// 数据字典:记每张表有哪些列、什么类型、多少行、有哪些索引,以及列级统计。
const STATS_DISTINCT_CAP: u64 = 4096;
const HIST_BUCKETS: usize = 16;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexSchema {
@@ -23,6 +24,12 @@ pub struct ColumnStat {
pub distinct: u64,
#[serde(default)]
pub capped: bool,
#[serde(default)]
pub lo: Option<f64>,
#[serde(default)]
pub hi: Option<f64>,
#[serde(default)]
pub hist: Vec<u64>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
@@ -152,6 +159,14 @@ impl Catalog {
if seen.remove(&fold(name, value_hash(v))) {
stat.distinct = stat.distinct.saturating_sub(1);
}
hist_remove(stat, v);
}
}
if entry.row_count == 0 {
for stat in entry.stats.values_mut() {
stat.lo = None;
stat.hi = None;
stat.hist.clear();
}
}
}
@@ -187,6 +202,7 @@ impl Catalog {
seen.clear();
}
}
hist_add(stat, v);
}
}
@@ -397,3 +413,63 @@ fn fold(column: &str, value_hash: u64) -> u64 {
value_hash.hash(&mut h);
h.finish()
}
/// 值落在第几个桶;区间退化时都算第 0 桶
fn hist_index(lo: f64, hi: f64, v: f64) -> usize {
if hi <= lo {
return 0;
}
let scaled = ((v - lo) / (hi - lo) * HIST_BUCKETS as f64).floor();
scaled.clamp(0.0, (HIST_BUCKETS - 1) as f64) as usize
}
/// 区间拓宽时把旧桶按中点并进来,只保总量
fn hist_merge(old: &[u64], old_lo: f64, old_hi: f64, new_lo: f64, new_hi: f64) -> Vec<u64> {
let mut out = vec![0u64; HIST_BUCKETS];
for (i, count) in old.iter().enumerate() {
if *count == 0 {
continue;
}
let mid = if old_hi > old_lo {
old_lo + (old_hi - old_lo) * (i as f64 + 0.5) / old.len() as f64
} else {
old_lo
};
out[hist_index(new_lo, new_hi, mid)] += *count;
}
out
}
/// 把一个值并进直方图;值跑出区间就拓宽区间
fn hist_add(stat: &mut ColumnStat, v: &serde_json::Value) {
let Some(x) = v.as_f64() else { return };
match (stat.lo, stat.hi) {
(Some(lo), Some(hi)) if stat.hist.len() == HIST_BUCKETS => {
if x < lo || x > hi {
let (nlo, nhi) = (lo.min(x), hi.max(x));
stat.hist = hist_merge(&stat.hist, lo, hi, nlo, nhi);
stat.lo = Some(nlo);
stat.hi = Some(nhi);
}
let index = hist_index(stat.lo.unwrap_or(x), stat.hi.unwrap_or(x), x);
stat.hist[index] += 1;
}
_ => {
stat.lo = Some(x);
stat.hi = Some(x);
stat.hist = vec![0u64; HIST_BUCKETS];
stat.hist[0] = 1;
}
}
}
/// 从直方图里减掉一个值;值在区间外就不动
fn hist_remove(stat: &mut ColumnStat, v: &serde_json::Value) {
let Some(x) = v.as_f64() else { return };
let (Some(lo), Some(hi)) = (stat.lo, stat.hi) else { return };
if x < lo || x > hi || stat.hist.len() != HIST_BUCKETS {
return;
}
let index = hist_index(lo, hi, x);
stat.hist[index] = stat.hist[index].saturating_sub(1);
}
+7 -1
View File
@@ -58,12 +58,18 @@ fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ColumnStatWire {
pub name: String,
pub distinct: u64,
#[serde(default)]
pub capped: bool,
#[serde(default)]
pub lo: Option<f64>,
#[serde(default)]
pub hi: Option<f64>,
#[serde(default)]
pub hist: Vec<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+3
View File
@@ -1602,6 +1602,9 @@ fn stat_wires(schema: &crate::catalog::TableSchema) -> Vec<ColumnStatWire> {
name: name.clone(),
distinct: s.distinct,
capped: s.capped,
lo: s.lo,
hi: s.hi,
hist: s.hist.clone(),
})
.collect()
}
+41
View File
@@ -83,6 +83,47 @@ fn remove_column_drops_definition_stats_and_index() {
assert!(c.remove_column("nope", "code").is_err());
}
/// 直方图随插入累计、随删除递减,值跑出区间就拓宽
#[test]
fn stats_track_a_histogram_for_numeric_columns() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("catalog.json");
let mut c = Catalog::load(&path).unwrap();
let rows: Vec<Row> = (0..100)
.map(|n| row(&[("id", json!(n)), ("name", json!(format!("u{}", n)))]))
.collect();
c.rebuild_stats("t", &rows);
let s = c.describe("t").unwrap();
let id = s.stats.get("id").unwrap();
assert_eq!(id.distinct, 100);
assert_eq!(id.lo, Some(0.0));
assert_eq!(id.hi, Some(99.0));
assert_eq!(id.hist.len(), 16);
assert_eq!(id.hist.iter().sum::<u64>(), 100);
let name = s.stats.get("name").unwrap();
assert!(name.hist.is_empty(), "字符串列不进直方图");
assert_eq!(name.lo, None);
c.record_insert("t", &row(&[("id", json!(1000)), ("name", json!("x"))]));
let s = c.describe("t").unwrap();
let id = s.stats.get("id").unwrap();
assert_eq!(id.lo, Some(0.0));
assert_eq!(id.hi, Some(1000.0));
assert_eq!(id.hist.iter().sum::<u64>(), 101);
c.record_delete("t", &[row(&[("id", json!(1000)), ("name", json!("x"))])]);
let s = c.describe("t").unwrap();
assert_eq!(s.stats.get("id").unwrap().hist.iter().sum::<u64>(), 100);
c.record_delete("t", &rows);
let s = c.describe("t").unwrap();
let id = s.stats.get("id").unwrap();
assert_eq!(id.lo, None, "删空之后直方图清掉");
assert_eq!(id.hist.iter().sum::<u64>(), 0);
}
/// 老类型名并到新写法,参数保留,未知类型原样
#[test]
fn normalize_type_maps_legacy_names() {
@@ -1554,6 +1554,11 @@ fn stats_report_distinct_values() {
assert!(r.contains(r#""row_count":3"#), "row_count: {}", r);
assert!(r.contains(r#""name":"age","distinct":2"#), "age has 2 distinct values: {}", r);
assert!(r.contains(r#""name":"id","distinct":3"#), "id has 3 distinct values: {}", r);
assert!(
r.contains(r#""name":"age","distinct":2,"capped":false,"lo":30.0,"hi":41.0"#),
"age carries a histogram range: {}",
r
);
send(&mut c, r#"{"method":"delete_keys","table":"stat_t","keys":[1]}"#);
let r = send(&mut c, r#"{"method":"describe_table","table":"stat_t"}"#);
@@ -43,4 +43,4 @@ accountColumnSpec name ty nullable autoIncrement primary unique =
-- | 账号表的结构(排序校验用)
systemTableInfo :: TableInfo
systemTableInfo = TableInfo accountTableName accountColumns 0 [] []
systemTableInfo = TableInfo accountTableName accountColumns 0 [] [] []
+6 -2
View File
@@ -319,7 +319,7 @@ exprTables expr = case expr of
-- | 两侧都取表名
both a b = exprTables a ++ exprTables b
-- | 一张表的线上信息(内存实现:没有索引,统计现算)
-- | 一张表的线上信息(内存实现:没有索引,统计与直方图现算)
tableInfoOf :: (String, Table) -> TableInfo
tableInfoOf (name, tbl) =
TableInfo
@@ -328,10 +328,14 @@ tableInfoOf (name, tbl) =
, tiRows = length (tableRows tbl)
, tiIndexes = []
, tiStats = [(c, distinctOf c, False) | (c, _) <- tableCols tbl]
, tiHistograms = [(c, h) | (c, _) <- tableCols tbl, Just h <- [histogramOf (columnValues c)]]
}
where
-- | 一列的所有值
columnValues c = [v | r <- tableRows tbl, Just v <- [lookup c r]]
-- | 一列的不同值个数
distinctOf c = length (nub [v | r <- tableRows tbl, Just v <- [lookup c r]])
distinctOf c = length (nub (columnValues c))
-- | 本地列类型转线上字符串(和 IPC 那边同一套写法)
wireType :: Column -> String
+17 -1
View File
@@ -6,13 +6,14 @@ module Main (main) where
import ChuSQL.Core.Engine.Storage.IPC (
Account (..),
Request (ReqAccountReset),
TableInfo (..),
closeConnection,
localStorageLink,
setStorageLink,
)
import ChuSQL.Core.Engine.Syntax.AST (Statement)
import ChuSQL.Core.Engine.Syntax.Parser (parseStatement)
import ChuSQL.Core.Model (Database, Row, Table (..), Value (..), pattern TInt, pattern TStr)
import ChuSQL.Core.Model (Database, Histogram (..), Row, Table (..), Value (..), pattern TInt, pattern TStr)
import ChuSQL.Interface.Auth (hashPasswordWith)
import ChuSQL.Interface.Protocol (ClientRequest (..), Grant (..), RoleView (..), ServerResponse (..), decodeRequest, encodeResponse)
import ChuSQL.Interface.RateLimit (newRateLimiter)
@@ -307,6 +308,21 @@ tcpSpec = do
asBool (at "truncated" value) `shouldBe` False
asText (at "database" value) `shouldBe` "test"
describe "memory backend: catalog statistics" $ do
it "reports a histogram for a numeric column" $ do
db <- newMVar testDb
let backend = memoryBackend testDatabaseName db
infos <- beCatalog backend
case infos of
Left err -> expectationFailure err
Right tables -> case [i | i <- tables, tiTable i == "users"] of
[] -> expectationFailure "the catalog is missing the users table"
(info : _) -> do
fmap histLow (lookup "id" (tiHistograms info)) `shouldBe` Just 1
fmap histHigh (lookup "id" (tiHistograms info)) `shouldBe` Just 5
fmap (sum . histBuckets) (lookup "id" (tiHistograms info)) `shouldBe` Just 5
lookup "name" (tiHistograms info) `shouldBe` Nothing
describe "server config ([server] in chusql.toml)" $ do
it "falls back to the defaults when the file has no server section" $ do
path <- tcpConfigPath "server-default"
+2
View File
@@ -53,6 +53,8 @@ chusql-server
量词比较走另一个真节点 `QuantCmp`:解析时普通比较运算符先读最长符号,发现后面是 `ANY` / `ALL` 就让给量词解析,因此 `age > ANY (SELECT …)` 与 `age > 25` 共用同一层优先级。求值时先把子查询的行取成字面量,再摊成 `AND`(`ALL`)/ `OR`(`ANY`)链交给既有三值逻辑,于是空子查询 `ANY` 为假、`ALL` 为真,子查询里的 `NULL` 按三值规则传播。节点不带取反位(本方言没有前缀 `NOT`),取反写相反的运算符,如 `<> ALL` 与 `NOT IN` 同义。
优化器的成本估算用存储维护的统计:行数、列级不同值数,以及数值列的列级等宽直方图。直方图是 16 个桶加区间下界 / 上界,只统计 `int` 与 `float` 列(NULL 与非数值不进直方图);存储侧随插入累计、随删除递减,值跑出区间就把旧桶按中点并进新区间,删到表空时清空。这些统计随 schema 下沉到引擎,`Cost.rangeRows` 有直方图就按桶与区间的交集估命中行数(端点按闭区间算),没有直方图退回原来的四分之一;`Cost.selectivityOf` 把这个比例交给连接顺序估算与索引改写的成本把关,因此一条覆盖大半张表的范围条件不会再被改写成索引点查。在线协议里每条列统计多出 `lo` / `hi` / `hist` 三个可选字段,缺省即「没有直方图」,老客户端与老字典都能照常读。
显式事务在会话层实现:`BEGIN` 把当前数据库整库读成内存快照,事务内的语句都在快照上执行;`SAVEPOINT` 把当前快照压进会话里的保存点栈,`ROLLBACK TO` 退回该点并保留它,`RELEASE` 丢掉该点与其后的保存点;`COMMIT` 先把快照、基线与库里现值按行 `id` 逐键比对,检测到写写冲突就报 `serialization_failure` 并保留事务;没有冲突才把差分用一条 `apply_transaction` 请求交给存储层应用;`ROLLBACK` 直接丢弃快照。因此隔离级别是快照读加原子提交:事务里看不到别的事务未提交或后来提交的改动,提交对外一次性可见,冲突由调用方回滚或重试。
WAL 负责崩溃恢复:每条记录带自增 LSN,一组写操作与一条提交标记一次落盘;每组提交在数据和字典都刷盘之后把 `wal.checkpoint` 推进到这条提交的 LSN,含义是「到此为止的改动都已落盘」,再按这个边界丢掉日志前缀——所以日志里通常只有当组在飞的记录。进程重启时只重放检查点之后的记录,按标记分组取已提交的组、丢弃没有标记的整组,重放前逐条做幂等检查作为第二道保险,结束后再把检查点推到最后一条提交。LSN 从检查点与文件尾里较大的那个续号,跨重启单调,`SystemStatus` 应答里的 `last_lsn` 报告该检查点 LSN。建表、删表、建索引、删索引与数据改动走同一条 WAL,恢复时一并重放。