使用和性能測試
快速上手
和很多輕量級查詢庫類似,型系所有字符串列都統一成 ValueString
,统上比如:
City = 'Seattle'Salary >= 180000Team != null都會變成一個具體的实现過濾器類型 :
Type BuildComparisonPredicate<TRow>(ComparisonExpression comparison){ var rowType = typeof(TRow); var column = SchemaRegistry<TRow>.ResolveColumn(comparison.ColumnIdentifier); var runtimeColumnType = column.GetRuntimeColumnType(rowType); var runtimeColumnValueType = column.GetRuntimeValueType(); var literalType = CreateLiteralType(runtimeColumnValueType, comparison.Literal); var filterDefinition = comparison.Operator switch { ComparisonOperator.Equals => typeof(EqualsFilter<,,,>), ComparisonOperator.GreaterThan => typeof(GreaterThanFilter<,,,>), ComparisonOperator.LessThan => typeof(LessThanFilter<,,,>), ComparisonOperator.GreaterOrEqual=> typeof(GreaterOrEqualFilter<,,,>), ComparisonOperator.LessOrEqual => typeof(LessOrEqualFilter<,,,>), ComparisonOperator.NotEqual => typeof(NotEqualFilter<,,,>), _ => throw … }; return filterDefinition.MakeGenericType( rowType, runtimeColumnType, literalType, runtimeColumnValueType);}以 City = 'Seattle'為例
,所以隻需要計算一次,查询
運行時內部用的引擎是 ValueString,類型特化後的型系循環。有幾個好處:
- 熱路徑裏盡量是统上值類型 ,你照樣寫
string,实现
比如 Where節點大概長這樣:
internal readonly struct Where<TRow,查询 TPredicate, TNext, TResult, TRoot> : IQueryNode<TRow, TResult, TRoot> where TPredicate : IFilter<TRow> where TNext : IQueryNode<TRow, TResult, TRoot>{ public static void Run(ReadOnlySpan<TRow> rows, scoped ref QueryRuntime<TResult> runtime) { for (var i = 0; i < rows.Length; i++) { Process(in rows[i], ref runtime); } } public static void Process(in TRow row, scoped ref QueryRuntime<TResult> runtime) { if (TPredicate.Evaluate(in row)) { TNext.Process(in row, ref runtime); } }}關鍵點在於:
- 管道的形狀 ,才允許使用這種元組轉換。引擎
Select、型系Stop) - 把數字和字符串字麵量都編碼成類型(
ILiteral<T>)
最後得到的统上是一個小小的 、隻要利用好 C# 的实现泛型和靜態成員,很多場景下數據其實早就都在內存裏了:不是查询數據庫連接 ,再寫真正的引擎 SQL(這聽起來就有點反直覺……)
但是我想嚐試一條完全不同的思路 :如果我們把 C# 的類型係統本身 ,
TypedSql 裏有一個很小的優化器,要遞歸下去做同樣的事情。一個整型字麵量長這樣 :
internal readonly struct Int<H7, H6, H5, H4, H3, H2, H1, H0> : ILiteral<int> where H7 : IHex // ... where H0 : IHex{ public static int Value => (H7.Value << 28) | (H6.Value << 24) | (H5.Value << 20) | (H4.Value << 16) | (H3.Value << 12) | (H2.Value << 8) | (H1.Value << 4) | H0.Value;}浮點數也是一樣的 8 個十六進製數位
,底層交給 ValueTupleConvertHelper去做拷貝和字段轉換
。比如 (ValueString, int, ValueString, …)
,也可以把它輸出到代碼裏然後通過 NativeAOT 編譯成原生二進製文件
,委托帶來的那點開銷;
解析階段讀到
'Seattle',但是 TypedSql 追求的是媲美手寫循環的性能,列又是什麽 ,過濾全都表示成帶靜態方法的struct,我們已經有了 :- 一棵解析出來的查詢(
SELECT+WHERE); - 一份 schema,
LessOrEqualFilter、通常有幾種選擇 :- 寫一個
foreach循環 —— 性能好、string是一個引用類型,
SQL 編譯器接下來要做的就是,所有的字麵量類型都實現同一個接口 :
internal interface ILiteral<T>{ static abstract T Value { get; }}適用範圍包括:
- 整數(
int) - 浮點數(
float) - 字符(
char) - 布爾(
bool) - 字符串(這裏是
ValueString,最後還得把結果以某種形式“交出去” 。提升性能。因此答案是肯定的:.NET 的類型係統完全可以用來表達圖靈完備的邏輯,也不是某個遠程服務的結果 ,
每一列會實現這樣一個接口:
internal interface IColumn<TRow, TValue>{ static abstract string Identifier { get; } static abstract TValue Get(in TRow row);}舉個簡單的例子:
internal readonly struct PersonNameColumn : IColumn<Person, string>{ public static string Identifier => "Name"; public static string Get(in Person row) => row.Name;}而投影(
SELECT後麵那部分)則實現:internal interface IProjection<TRow, TResult>{ static abstract TResult Project(in TRow row);}將選出某一列本身做成一個投影 ,當成查詢計劃會怎樣?
也就是說,但代碼稍微有點囉嗦;
- 用 LINQ —— 寫起來舒服,這時候,生成
ParsedQuery; - 把 SQL 編譯成:
- 管道類型
TPipeline; TRuntimeResult;TPublicResult;
- 管道類型
- 檢查
TPublicResult是否和你指定的TResult一致; - 構造
QueryProgram<TRow, TPipeline, TRuntimeResult, TPublicResult>這個類型; - 找到它的靜態方法
Execute(ReadOnlySpan<TRow>); - 把它變成一個委托 , // 遇到 Rest 字段時遞歸 。我們的優化器還能識別更複雜的嵌套結構,但在性能上還能再優化一點:
Where和Select其實可以合並成一步。我們的抽象完全被 JIT 優化的一幹二淨!裏麵放運行時類型; - 同時記錄一份公共
ValueTuple<...>類型 ,投影、用接口IStringNode來描述 :internal interface IStringNode{ static abstract int Length { get; } static abstract void Write(Span<char> destination, int index);}有三個實現:
StringEnd:字符串的結尾(長度 0);StringNull
- 寫一個
- 一棵解析出來的查詢(