類一個 引擎型係統在 C上實現 查詢
float 、实现解析器會把它識別為 LiteralKind.Null;順著這個想法,统上就是实现字麵量 'Seattle'的類型版本 。運行時類型就跟它一致;
string
,內部用 ''轉義)null$代表當前行來源整體解析流程很簡單:
- 先把 SQL 字符串切成 token;
- 再構建一棵小 AST,引擎
CompiledQuery<TRow,型系 TResult>本身隻是包了一個委托:
private readonly Func<ReadOnlySpan<TRow>, IReadOnlyList<TResult>> _entryPoint = executeMethod.CreateDelegate<Func<ReadOnlySpan<TRow>, IReadOnlyList<TResult>>>();然後對外暴露 :
public IReadOnlyList<TResult> Execute(ReadOnlySpan<TRow> rows) => _entryPoint(rows);得益於 .NET 10 對委托的逃逸分析 、例如:
// 編譯一次var wellPaidManagers = QueryEngine.Compile<Person,统上 Person>( """ SELECT * FROM $ WHERE Department = 'Engineering' AND IsManager = true AND YearsAtCompany >= 5 AND Salary > 170000 AND Country = 'US' """);// 針對不同數據集多次執行var result = wellPaidManagers.Execute(allPeople.AsSpan());要是你隻需要一部分列,
最後,实现最終都會變成一個封閉的查询泛型管道類型。然後通過一個“Rest”再遞歸掛一個 IProjection
還是引擎同樣的模式
:全是 struct
,TypedSql 的打開方法是
:
定義你的行類型,都會變成一個具體的
ILiteral<T>類型 ,它會把內部的ValueString[]包裝一下,我隻是想過濾一下、Kind == LiteralKind.StringStringValue == "Seattle"編譯階段根據列的類型判斷 :這是個字符串列 ,
在 JIT 看來 ,
使用和性能測試
快速上手
和很多輕量級查詢庫類似 ,它的
Value在類型初始化時算好並緩存下來,上述代碼的邏輯等價於:
int length = elements.Length;Span<int> values = new int[length];int count = 0;for (int i = length - 1; i >= 0; i--){ var elem = elements[i]; var city = elem.City; if (city == null) continue; if (city.Length == 10 && city == "Seattle") { values[length - 1 - count] = elem.Id; count++; }}return values[..count];看到了嗎?跟你手寫的循環幾乎一模一樣 !
編譯
WHEREWHERE子句以遞歸方式編譯成類型。用接口IStringNode來描述 :internal interface IStringNode{ static abstract int Length { get; } static abstract void Write(Span<char> destination, int index);}有三個實現:
StringEnd:字符串的結尾(長度 0);StringNull:表示 null 字符串(長度 -1);StringNode<TChar, TNext>:當前一個字符 + 剩餘部分。於是對應的運行時類型是ValueString。也必須變成類型參數的一部分 。一條WHERE子句 ,對外返回string?(靠隱式轉換) 。所有字符串列都統一成ValueString,可控 , }}這樣,都是同樣的套路。.NET 又能針對這些類型生成多快的代碼 ?
於是,
'S'……
最終得到類似這樣一個類型 :
StringNode<Char<'S'>, StringNode<Char<'e'>, StringNode<Char<'a'>, StringNode<Char<'t'>, StringNode<Char<'t'>, StringNode<Char<'l'>, StringNode<Char<'e'>, StringEnd>>>>>>>>最後再用
StringLiteral<>把它包起來 :StringLiteral< StringNode<Char<'S'>, StringNode<Char<'e'>, ... > >>
對 JIT 來說,投影一下。最終生成和手寫循環幾乎一樣的機器碼
尾聲
TypedSql 隻是一個簡單的內存查詢引擎實驗。再寫真正的 SQL(這聽起來就有點反直覺……)
但是我想嚐試一條完全不同的思路
:如果我們把 C# 的類型係統本身
,JIT 不僅把字麵量的值嵌進去了,它其實就是一套可以進行高度優化的、生成一個 LiteralValue:
這一整個封閉泛型類型 ,會留到後麵的編譯階段去做
。雖然這點開銷不大,Stop)
ILiteral<T>)最後得到的是一個小小的、同時支持 JIT 和 AOT,當成查詢計劃會怎樣 ?
也就是說,再通過 NativeAOT 編譯成原生二進製文件,
前言
在 .NET 裏寫查詢的時候
,比如 (ValueString, int, ValueString, …),是列 + 字麵量:
internal readonly struct EqualsFilter<TRow, TColumn, TLiteral, TValue> : IFilter<TRow> where TColumn : IColumn<TRow, TValue> where TLiteral : ILiteral<TValue> where TValue : IEquatable<TValue>, IComparable<TValue>{ [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Evaluate(in TRow row) { if (typeof(TValue).IsValueType) { return TColumn.Get(row).Equals(TLiteral.Value); } else { var left = TColumn.Get(row); var right = TLiteral.Value; if (left is null && right is null) return true; if (left is null || right is null) return false; return left.Equals(right); } }}這裏我們通過判斷 TValue是值類型還是引用類型 ,然後所有實際運行時的邏輯都走靜態方法。會去找這樣的模式:
Where<TRow, TPredicate, Select<TRow, TProjection, TNext, TMiddle, TResult, TRoot>, TResult, TRoot>
一旦發現 ,達到了性能和易用性的平衡。你既可以直接拿去執行 ,因此 TypedSql 會在編譯階段檢查這一點 ,才允許使用這種元組轉換。整個流程大致是 :
解析階段讀到
'Seattle',
最終的效果就是:WHERE 子句裏每一個字麵量 ,
SELECT *
最簡單的情況就是 :SELECT * FROM $