From b7a29cfcc15fc1da620a1524cfffcbc6ef1ce29a Mon Sep 17 00:00:00 2001 From: faddiv Date: Tue, 30 Sep 2025 18:39:53 +0200 Subject: [PATCH 1/2] Add benchmarks for QueryBuilder with TestCompiler and SelectsBenchmark --- .../Infrastructure/TestCompiler.cs | 9 ++ .../Infrastructure/TestSupport.cs | 49 ++++++++++ QueryBuilder.Benchmarks/Program.cs | 8 ++ .../QueryBuilder.Benchmarks.csproj | 19 ++++ QueryBuilder.Benchmarks/SelectsBenchmark.cs | 82 ++++++++++++++++ .../SelectsBenchmarkTests.cs | 95 +++++++++++++++++++ sqlkata.sln | 26 +++++ 7 files changed, 288 insertions(+) create mode 100644 QueryBuilder.Benchmarks/Infrastructure/TestCompiler.cs create mode 100644 QueryBuilder.Benchmarks/Infrastructure/TestSupport.cs create mode 100644 QueryBuilder.Benchmarks/Program.cs create mode 100644 QueryBuilder.Benchmarks/QueryBuilder.Benchmarks.csproj create mode 100644 QueryBuilder.Benchmarks/SelectsBenchmark.cs create mode 100644 QueryBuilder.Benchmarks/SelectsBenchmarkTests.cs diff --git a/QueryBuilder.Benchmarks/Infrastructure/TestCompiler.cs b/QueryBuilder.Benchmarks/Infrastructure/TestCompiler.cs new file mode 100644 index 00000000..171524ac --- /dev/null +++ b/QueryBuilder.Benchmarks/Infrastructure/TestCompiler.cs @@ -0,0 +1,9 @@ +using SqlKata.Compilers; + +namespace QueryBuilder.Benchmarks.Infrastructure; + +public class TestCompiler + : Compiler +{ + public override string EngineCode { get; } = "generic"; +} diff --git a/QueryBuilder.Benchmarks/Infrastructure/TestSupport.cs b/QueryBuilder.Benchmarks/Infrastructure/TestSupport.cs new file mode 100644 index 00000000..2e953a51 --- /dev/null +++ b/QueryBuilder.Benchmarks/Infrastructure/TestSupport.cs @@ -0,0 +1,49 @@ +using SqlKata; +using SqlKata.Compilers; + +namespace QueryBuilder.Benchmarks.Infrastructure; + +public class TestSupport +{ + + public static SqlResult CompileFor(string engine, Query query, Func configuration = null) + { + var compiler = CreateCompiler(engine); + if (configuration != null) + { + compiler = configuration(compiler); + } + + return compiler.Compile(query); + } + + public static SqlResult CompileFor(string engine, Query query, Action configuration) + { + return CompileFor(engine, query, compiler => + { + configuration(compiler); + return compiler; + }); + } + + public static Compiler CreateCompiler(string engine) + { + return engine switch + { + EngineCodes.Firebird => new FirebirdCompiler(), + EngineCodes.MySql => new MySqlCompiler(), + EngineCodes.Oracle => new OracleCompiler + { + UseLegacyPagination = false + }, + EngineCodes.PostgreSql => new PostgresCompiler(), + EngineCodes.Sqlite => new SqliteCompiler(), + EngineCodes.SqlServer => new SqlServerCompiler + { + UseLegacyPagination = false + }, + EngineCodes.Generic => new TestCompiler(), + _ => throw new ArgumentException($"Unsupported engine type: {engine}", nameof(engine)), + }; + } +} diff --git a/QueryBuilder.Benchmarks/Program.cs b/QueryBuilder.Benchmarks/Program.cs new file mode 100644 index 00000000..68481a35 --- /dev/null +++ b/QueryBuilder.Benchmarks/Program.cs @@ -0,0 +1,8 @@ +// See https://aka.ms/new-console-template for more information + +using BenchmarkDotNet.Running; +using QueryBuilder.Benchmarks; + +SelectsBenchmarkTests.TestAll(); + +BenchmarkRunner.Run(); diff --git a/QueryBuilder.Benchmarks/QueryBuilder.Benchmarks.csproj b/QueryBuilder.Benchmarks/QueryBuilder.Benchmarks.csproj new file mode 100644 index 00000000..2960e8f2 --- /dev/null +++ b/QueryBuilder.Benchmarks/QueryBuilder.Benchmarks.csproj @@ -0,0 +1,19 @@ + + + + Exe + net8.0 + enable + enable + false + + + + + + + + + + + diff --git a/QueryBuilder.Benchmarks/SelectsBenchmark.cs b/QueryBuilder.Benchmarks/SelectsBenchmark.cs new file mode 100644 index 00000000..e81e7bef --- /dev/null +++ b/QueryBuilder.Benchmarks/SelectsBenchmark.cs @@ -0,0 +1,82 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.RegularExpressions; +using BenchmarkDotNet.Attributes; +using QueryBuilder.Benchmarks.Infrastructure; +using SqlKata; +using SqlKata.Compilers; + +namespace QueryBuilder.Benchmarks; + +[MemoryDiagnoser] +public class SelectsBenchmark +{ + private Query selectSimple; + private Query selectGroupBy; + private Query selectWith; + + public Compiler compiler; + + [Params( + EngineCodes.SqlServer)] + public string EngineCode { get; set; } + + [GlobalSetup] + public void Setup() + { + selectSimple = new Query("Products") + .Select("ProductID", "ProductName", "SupplierID", "CategoryID", "UnitPrice", "UnitsInStock", "UnitsOnOrder", + "ReorderLevel", "Discontinued") + .WhereIn("CategoryID", [1, 2, 3]) + .Where("SupplierID", 5) + .Where("UnitPrice", ">=", 10) + .Where("UnitPrice", "<=", 100) + .Take(10) + .Skip(20) + .OrderBy("UnitPrice", "ProductName"); + + + selectGroupBy = new Query("Products") + .Select("SupplierID", "CategoryID") + .SelectAvg("UnitPrice") + .SelectMin("UnitPrice") + .SelectMax("UnitPrice") + .Where("CategoryID", 123) + .GroupBy("SupplierID", "CategoryID") + .HavingRaw("MIN(UnitPrice) >= ?", 10) + .Take(10) + .Skip(20) + .OrderBy("SupplierID", "CategoryID"); + + var activePosts = new Query("Comments") + .Select("PostId") + .SelectRaw("count(1) as Count") + .GroupBy("PostId") + .HavingRaw("count(1) > 100"); + + selectWith = new Query("Posts") + .With("ActivePosts", activePosts) + .Join("ActivePosts", "ActivePosts.PostId", "Posts.Id") + .Select("Posts.*", "ActivePosts.Count"); + + compiler = TestSupport.CreateCompiler(EngineCode); + } + + [Benchmark] + public SqlResult SelectSimple() + { + return compiler.Compile(selectSimple); + } + + [Benchmark] + public SqlResult SelectGroupBy() + { + return compiler.Compile(selectGroupBy); + } + + [Benchmark] + public SqlResult SelectWith() + { + return compiler.Compile(selectWith); + } + +} diff --git a/QueryBuilder.Benchmarks/SelectsBenchmarkTests.cs b/QueryBuilder.Benchmarks/SelectsBenchmarkTests.cs new file mode 100644 index 00000000..36aafac0 --- /dev/null +++ b/QueryBuilder.Benchmarks/SelectsBenchmarkTests.cs @@ -0,0 +1,95 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.RegularExpressions; +using SqlKata; +using SqlKata.Compilers; + +namespace QueryBuilder.Benchmarks; + +public static partial class SelectsBenchmarkTests +{ + public static void TestAll() + { + TestSelectSimple(); + TestSelectGroupBy(); + TestSelectWith(); + } + + public static void TestSelectSimple() + { + var benchmark = CreateBenchmark(); + + var result = benchmark.SelectSimple(); + + // language=SQL + ValidateResult( + """ + SELECT [ProductID], [ProductName], [SupplierID], [CategoryID], [UnitPrice], + [UnitsInStock], [UnitsOnOrder], [ReorderLevel], [Discontinued] + FROM [Products] + WHERE [CategoryID] IN (1, 2, 3) + AND [SupplierID] = 5 + AND [UnitPrice] >= 10 + AND [UnitPrice] <= 100 + ORDER BY [UnitPrice], [ProductName] + OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY + """, result); + } + + public static void TestSelectGroupBy() + { + var benchmark = CreateBenchmark(); + + var result = benchmark.SelectGroupBy(); + + // language=SQL + ValidateResult( + """ + SELECT [SupplierID], [CategoryID], + AVG([UnitPrice]), MIN([UnitPrice]), MAX([UnitPrice]) + FROM [Products] + WHERE [CategoryID] = 123 + GROUP BY [SupplierID], [CategoryID] + HAVING MIN(UnitPrice) >= 10 + ORDER BY [SupplierID], [CategoryID] + OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY + """, result); + } + + public static void TestSelectWith() + { + var benchmark = CreateBenchmark(); + + var result = benchmark.SelectWith(); + + // language=SQL + ValidateResult( + """ + WITH [ActivePosts] AS (SELECT [PostId], count(1) as Count FROM [Comments] GROUP BY [PostId] HAVING count(1) > 100) + SELECT [Posts].*, [ActivePosts].[Count] + FROM [Posts] + INNER JOIN [ActivePosts] ON [ActivePosts].[PostId] = [Posts].[Id] + """, result); + } + + private static SelectsBenchmark CreateBenchmark() + { + var benchmark = new SelectsBenchmark + { + EngineCode = EngineCodes.SqlServer + }; + benchmark.Setup(); + return benchmark; + } + + private static void ValidateResult(string expected, SqlResult result) + { + var actual = result.ToString(); + if (WhiteSpaces().Replace(actual, " ") != WhiteSpaces().Replace(expected, " ")) + { + throw new ValidationException($"Invalid result: {actual}"); + } + } + + [GeneratedRegex(@"\s+")] + private static partial Regex WhiteSpaces(); +} diff --git a/sqlkata.sln b/sqlkata.sln index ef91408a..2a3db5f6 100644 --- a/sqlkata.sln +++ b/sqlkata.sln @@ -14,6 +14,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .editorconfig = .editorconfig EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QueryBuilder.Benchmarks", "QueryBuilder.Benchmarks\QueryBuilder.Benchmarks.csproj", "{4C4BB0A9-0FD3-455D-B4C4-2BB43C5C7388}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -60,6 +62,30 @@ Global {B6DF0569-6040-4EAF-A38B-E4DEB8DC76E0}.Release|x64.Build.0 = Release|Any CPU {B6DF0569-6040-4EAF-A38B-E4DEB8DC76E0}.Release|x86.ActiveCfg = Release|Any CPU {B6DF0569-6040-4EAF-A38B-E4DEB8DC76E0}.Release|x86.Build.0 = Release|Any CPU + {5DEA7DBC-5B8A-44A9-A070-55E95881A4CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5DEA7DBC-5B8A-44A9-A070-55E95881A4CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5DEA7DBC-5B8A-44A9-A070-55E95881A4CF}.Debug|x64.ActiveCfg = Debug|Any CPU + {5DEA7DBC-5B8A-44A9-A070-55E95881A4CF}.Debug|x64.Build.0 = Debug|Any CPU + {5DEA7DBC-5B8A-44A9-A070-55E95881A4CF}.Debug|x86.ActiveCfg = Debug|Any CPU + {5DEA7DBC-5B8A-44A9-A070-55E95881A4CF}.Debug|x86.Build.0 = Debug|Any CPU + {5DEA7DBC-5B8A-44A9-A070-55E95881A4CF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5DEA7DBC-5B8A-44A9-A070-55E95881A4CF}.Release|Any CPU.Build.0 = Release|Any CPU + {5DEA7DBC-5B8A-44A9-A070-55E95881A4CF}.Release|x64.ActiveCfg = Release|Any CPU + {5DEA7DBC-5B8A-44A9-A070-55E95881A4CF}.Release|x64.Build.0 = Release|Any CPU + {5DEA7DBC-5B8A-44A9-A070-55E95881A4CF}.Release|x86.ActiveCfg = Release|Any CPU + {5DEA7DBC-5B8A-44A9-A070-55E95881A4CF}.Release|x86.Build.0 = Release|Any CPU + {4C4BB0A9-0FD3-455D-B4C4-2BB43C5C7388}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C4BB0A9-0FD3-455D-B4C4-2BB43C5C7388}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C4BB0A9-0FD3-455D-B4C4-2BB43C5C7388}.Debug|x64.ActiveCfg = Debug|Any CPU + {4C4BB0A9-0FD3-455D-B4C4-2BB43C5C7388}.Debug|x64.Build.0 = Debug|Any CPU + {4C4BB0A9-0FD3-455D-B4C4-2BB43C5C7388}.Debug|x86.ActiveCfg = Debug|Any CPU + {4C4BB0A9-0FD3-455D-B4C4-2BB43C5C7388}.Debug|x86.Build.0 = Debug|Any CPU + {4C4BB0A9-0FD3-455D-B4C4-2BB43C5C7388}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C4BB0A9-0FD3-455D-B4C4-2BB43C5C7388}.Release|Any CPU.Build.0 = Release|Any CPU + {4C4BB0A9-0FD3-455D-B4C4-2BB43C5C7388}.Release|x64.ActiveCfg = Release|Any CPU + {4C4BB0A9-0FD3-455D-B4C4-2BB43C5C7388}.Release|x64.Build.0 = Release|Any CPU + {4C4BB0A9-0FD3-455D-B4C4-2BB43C5C7388}.Release|x86.ActiveCfg = Release|Any CPU + {4C4BB0A9-0FD3-455D-B4C4-2BB43C5C7388}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 2f691189fa65362d692f529da4747fa46c37d704 Mon Sep 17 00:00:00 2001 From: faddiv Date: Tue, 30 Sep 2025 19:09:22 +0200 Subject: [PATCH 2/2] Simplified the benchmarks and added documentation. --- .gitignore | 3 + .../Infrastructure/TestSupport.cs | 21 ---- QueryBuilder.Benchmarks/Program.cs | 8 +- QueryBuilder.Benchmarks/REAEDME.MD | 30 ++++++ QueryBuilder.Benchmarks/SelectsBenchmark.cs | 8 +- .../SelectsBenchmarkTests.cs | 95 ------------------- 6 files changed, 37 insertions(+), 128 deletions(-) create mode 100644 QueryBuilder.Benchmarks/REAEDME.MD delete mode 100644 QueryBuilder.Benchmarks/SelectsBenchmarkTests.cs diff --git a/.gitignore b/.gitignore index 50290793..63981ad1 100644 --- a/.gitignore +++ b/.gitignore @@ -276,3 +276,6 @@ __pycache__/ # Thumbs Thumbs.db + +# BenchmarkDotNet report +BenchmarkDotNet.Artifacts/ \ No newline at end of file diff --git a/QueryBuilder.Benchmarks/Infrastructure/TestSupport.cs b/QueryBuilder.Benchmarks/Infrastructure/TestSupport.cs index 2e953a51..9023e6f1 100644 --- a/QueryBuilder.Benchmarks/Infrastructure/TestSupport.cs +++ b/QueryBuilder.Benchmarks/Infrastructure/TestSupport.cs @@ -5,27 +5,6 @@ namespace QueryBuilder.Benchmarks.Infrastructure; public class TestSupport { - - public static SqlResult CompileFor(string engine, Query query, Func configuration = null) - { - var compiler = CreateCompiler(engine); - if (configuration != null) - { - compiler = configuration(compiler); - } - - return compiler.Compile(query); - } - - public static SqlResult CompileFor(string engine, Query query, Action configuration) - { - return CompileFor(engine, query, compiler => - { - configuration(compiler); - return compiler; - }); - } - public static Compiler CreateCompiler(string engine) { return engine switch diff --git a/QueryBuilder.Benchmarks/Program.cs b/QueryBuilder.Benchmarks/Program.cs index 68481a35..e5c36886 100644 --- a/QueryBuilder.Benchmarks/Program.cs +++ b/QueryBuilder.Benchmarks/Program.cs @@ -1,8 +1,4 @@ -// See https://aka.ms/new-console-template for more information - +// BenchmarkDotNet: https://benchmarkdotnet.org/ using BenchmarkDotNet.Running; -using QueryBuilder.Benchmarks; - -SelectsBenchmarkTests.TestAll(); -BenchmarkRunner.Run(); +BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); diff --git a/QueryBuilder.Benchmarks/REAEDME.MD b/QueryBuilder.Benchmarks/REAEDME.MD new file mode 100644 index 00000000..fdd30ecd --- /dev/null +++ b/QueryBuilder.Benchmarks/REAEDME.MD @@ -0,0 +1,30 @@ +# QueryBuilder.Benchmarks + +This project is a benchmark suite for measuring the performance of the SqlKata query builder library. It uses [BenchmarkDotNet](https://benchmarkdotnet.org/) to provide performance metrics for various query-building scenarios. + +## About + +- **Purpose:** Evaluate the performance of different SqlKata query operations. +- **Framework:** [BenchmarkDotNet](https://benchmarkdotnet.org/) is used for running and reporting benchmarks. +- **Scope:** Includes benchmarks for a few select scenarios. + +## How to Use + +1. **Build the Solution:** + Make sure the solution is built in Release mode for accurate results. + +2. **Run the Benchmarks:** + Execute the following command from the root of the repository: + + ```cmd + dotnet run -c Release --project .\QueryBuilder.Benchmarks\QueryBuilder.Benchmarks.csproj + ``` + +3. **Select Benchmarks:** + After running the command, a benchmark selector will appear. This is powered by BenchmarkDotNet's `BenchmarkSwitcher`, allowing you to choose which benchmarks to execute interactively. + +4. **View Results:** + BenchmarkDotNet will output the results to the console and generate detailed reports in the `BenchmarkDotNet.Artifacts` directory. + +## References +- [BenchmarkDotNet](https://benchmarkdotnet.org/) diff --git a/QueryBuilder.Benchmarks/SelectsBenchmark.cs b/QueryBuilder.Benchmarks/SelectsBenchmark.cs index e81e7bef..32d9f799 100644 --- a/QueryBuilder.Benchmarks/SelectsBenchmark.cs +++ b/QueryBuilder.Benchmarks/SelectsBenchmark.cs @@ -1,6 +1,4 @@ -using System.ComponentModel.DataAnnotations; -using System.Text.RegularExpressions; -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; using QueryBuilder.Benchmarks.Infrastructure; using SqlKata; using SqlKata.Compilers; @@ -16,8 +14,7 @@ public class SelectsBenchmark public Compiler compiler; - [Params( - EngineCodes.SqlServer)] + [Params(EngineCodes.SqlServer)] public string EngineCode { get; set; } [GlobalSetup] @@ -78,5 +75,4 @@ public SqlResult SelectWith() { return compiler.Compile(selectWith); } - } diff --git a/QueryBuilder.Benchmarks/SelectsBenchmarkTests.cs b/QueryBuilder.Benchmarks/SelectsBenchmarkTests.cs deleted file mode 100644 index 36aafac0..00000000 --- a/QueryBuilder.Benchmarks/SelectsBenchmarkTests.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.Text.RegularExpressions; -using SqlKata; -using SqlKata.Compilers; - -namespace QueryBuilder.Benchmarks; - -public static partial class SelectsBenchmarkTests -{ - public static void TestAll() - { - TestSelectSimple(); - TestSelectGroupBy(); - TestSelectWith(); - } - - public static void TestSelectSimple() - { - var benchmark = CreateBenchmark(); - - var result = benchmark.SelectSimple(); - - // language=SQL - ValidateResult( - """ - SELECT [ProductID], [ProductName], [SupplierID], [CategoryID], [UnitPrice], - [UnitsInStock], [UnitsOnOrder], [ReorderLevel], [Discontinued] - FROM [Products] - WHERE [CategoryID] IN (1, 2, 3) - AND [SupplierID] = 5 - AND [UnitPrice] >= 10 - AND [UnitPrice] <= 100 - ORDER BY [UnitPrice], [ProductName] - OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY - """, result); - } - - public static void TestSelectGroupBy() - { - var benchmark = CreateBenchmark(); - - var result = benchmark.SelectGroupBy(); - - // language=SQL - ValidateResult( - """ - SELECT [SupplierID], [CategoryID], - AVG([UnitPrice]), MIN([UnitPrice]), MAX([UnitPrice]) - FROM [Products] - WHERE [CategoryID] = 123 - GROUP BY [SupplierID], [CategoryID] - HAVING MIN(UnitPrice) >= 10 - ORDER BY [SupplierID], [CategoryID] - OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY - """, result); - } - - public static void TestSelectWith() - { - var benchmark = CreateBenchmark(); - - var result = benchmark.SelectWith(); - - // language=SQL - ValidateResult( - """ - WITH [ActivePosts] AS (SELECT [PostId], count(1) as Count FROM [Comments] GROUP BY [PostId] HAVING count(1) > 100) - SELECT [Posts].*, [ActivePosts].[Count] - FROM [Posts] - INNER JOIN [ActivePosts] ON [ActivePosts].[PostId] = [Posts].[Id] - """, result); - } - - private static SelectsBenchmark CreateBenchmark() - { - var benchmark = new SelectsBenchmark - { - EngineCode = EngineCodes.SqlServer - }; - benchmark.Setup(); - return benchmark; - } - - private static void ValidateResult(string expected, SqlResult result) - { - var actual = result.ToString(); - if (WhiteSpaces().Replace(actual, " ") != WhiteSpaces().Replace(expected, " ")) - { - throw new ValidationException($"Invalid result: {actual}"); - } - } - - [GeneratedRegex(@"\s+")] - private static partial Regex WhiteSpaces(); -}