Skip to content

Instantly share code, notes, and snippets.

@redknightlois
Created April 6, 2026 15:44
Show Gist options
  • Select an option

  • Save redknightlois/017531841ebe02fbc963a558aac1770c to your computer and use it in GitHub Desktop.

Select an option

Save redknightlois/017531841ebe02fbc963a558aac1770c to your computer and use it in GitHub Desktop.
RavenDB-25930: Benchmark showing compound field query plan degradation without the `right is null` fix in CoraxQueryBuilder.TrySetAsStreamingField()
#!/usr/bin/env dotnet-script
// Benchmark: RavenDB-25930 Compound Field Query Plan Degradation
//
// Demonstrates the performance impact of the `right is null` fix in
// CoraxQueryBuilder.TrySetAsStreamingField(). Without the fix, a compound
// field definition (Name, Count) incorrectly captures the query plan for
// `WHERE Name = '1' AND Count > 100 ORDER BY Count`, preventing the
// CoraxBooleanItem merge that produces an efficient UnaryMatch range scan.
//
// Usage:
// dotnet script bench-compound-field.csx [server-url]
// dotnet script bench-compound-field.csx http://localhost:8080
//
// Run against a server WITH the fix and WITHOUT the fix to compare.
#r "nuget: RavenDB.Client, 7.0.2"
using Raven.Client.Documents;
using Raven.Client.Documents.Indexes;
using Raven.Client.Documents.Operations;
using Raven.Client.Documents.Operations.Indexes;
using Raven.Client.Documents.Queries;
using Raven.Client.Documents.Queries.Timings;
using Raven.Client.Documents.Session;
using Raven.Client.ServerWide;
using Raven.Client.ServerWide.Operations;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
// ---------------------------------------------------------------------------
// Config
// ---------------------------------------------------------------------------
var serverUrl = Args.Count > 0 ? Args[0] : "http://localhost:8080";
const string DatabaseName = "Bench_RavenDB_25930";
const int TotalDocs = 500_000;
const int WarmupSeconds = 3;
const int MeasureSeconds = 10;
const string Rql = "from index 'BenchIndex' where Name = '1' and Count > 100 order by Count as long";
Console.WriteLine($"=== RavenDB-25930: Compound Field Query Plan Benchmark ===");
Console.WriteLine($"Server : {serverUrl}");
Console.WriteLine($"Database: {DatabaseName}");
Console.WriteLine($"Docs : {TotalDocs:N0}");
Console.WriteLine();
// ---------------------------------------------------------------------------
// Setup
// ---------------------------------------------------------------------------
var store = new DocumentStore { Urls = new[] { serverUrl }, Database = DatabaseName };
store.Initialize();
try
{
// Create database (delete if exists)
try { store.Maintenance.Server.Send(new DeleteDatabasesOperation(DatabaseName, hardDelete: true)); }
catch { /* ignore if not exists */ }
store.Maintenance.Server.Send(new CreateDatabaseOperation(new DatabaseRecord(DatabaseName)
{
Settings = new Dictionary<string, string>
{
["Indexing.Static.SearchEngineType"] = "Corax",
["Indexing.Auto.SearchEngineType"] = "Corax"
}
}));
// Create index with compound field
store.Maintenance.Send(new PutIndexesOperation(new IndexDefinition
{
Name = "BenchIndex",
Maps = { "from doc in docs.Docs select new { doc.Name, doc.Count }" },
Fields = new Dictionary<string, IndexFieldOptions>
{
["Count"] = new IndexFieldOptions { Storage = FieldStorage.No }
},
CompoundFields = new List<string[]> { new[] { "Name", "Count" } }
}));
// Insert data
Console.Write($"Inserting {TotalDocs:N0} documents... ");
var insertSw = Stopwatch.StartNew();
using (var bulk = store.BulkInsert())
{
for (int i = 0; i < TotalDocs; i++)
{
bulk.Store(new Doc { Name = (i % 100).ToString(), Count = i % 5_000 }, $"docs/{i}");
}
}
Console.WriteLine($"done ({insertSw.Elapsed.TotalSeconds:F1}s)");
// Wait for indexing
Console.Write("Waiting for indexing... ");
var idxSw = Stopwatch.StartNew();
while (true)
{
var stats = store.Maintenance.Send(new GetStatisticsOperation());
if (stats.StaleIndexes.Length == 0) break;
System.Threading.Thread.Sleep(250);
if (idxSw.Elapsed.TotalSeconds > 120)
{
Console.WriteLine("TIMEOUT waiting for indexing!");
return;
}
}
Console.WriteLine($"done ({idxSw.Elapsed.TotalSeconds:F1}s)");
Console.WriteLine();
// -----------------------------------------------------------------------
// Query Plan Inspection
// -----------------------------------------------------------------------
Console.WriteLine("=== Query Plan ===");
Console.WriteLine($"RQL: {Rql}");
Console.WriteLine();
using (var session = store.OpenSession(new SessionOptions { NoCaching = true }))
{
QueryTimings timings = null;
var result = session.Advanced.DocumentQuery<object>("BenchIndex")
.Timings(out timings)
.WhereEquals("Name", "1")
.AndAlso()
.WhereGreaterThan("Count", (long)100)
.OrderBy("Count", OrderingType.Long)
.ToList();
Console.WriteLine($"Results: {result.Count} documents");
if (timings?.QueryPlan != null)
{
var plan = (QueryInspectionNode)timings.QueryPlan;
PrintPlan(plan, 0);
bool hasUnaryMatch = FindNode(plan, "UnaryMatch");
Console.WriteLine();
Console.WriteLine(hasUnaryMatch
? "[OK] Query plan contains UnaryMatch (correct - range scan preserved)"
: "[DEGRADED] Query plan MISSING UnaryMatch (compound field captured the plan incorrectly)");
}
}
Console.WriteLine();
// -----------------------------------------------------------------------
// Server-side Timing Sample
// -----------------------------------------------------------------------
Console.WriteLine("=== Server-Side Timing Sample (5 queries) ===");
var serverTimings = new List<double>();
for (int i = 0; i < 5; i++)
{
using (var session = store.OpenSession(new SessionOptions { NoCaching = true }))
{
QueryTimings t = null;
_ = session.Advanced.DocumentQuery<object>("BenchIndex")
.Timings(out t)
.WhereEquals("Name", "1")
.AndAlso()
.WhereGreaterThan("Count", (long)100)
.OrderBy("Count", OrderingType.Long)
.ToList();
if (t != null)
{
serverTimings.Add(t.DurationInMs);
Console.WriteLine($" Query {i + 1}: {t.DurationInMs:F2} ms (server-side)");
}
}
}
if (serverTimings.Count > 0)
{
Console.WriteLine($" Avg: {serverTimings.Average():F2} ms Min: {serverTimings.Min():F2} ms Max: {serverTimings.Max():F2} ms");
}
Console.WriteLine();
// -----------------------------------------------------------------------
// Throughput Benchmark
// -----------------------------------------------------------------------
Console.WriteLine($"=== Throughput Benchmark ===");
Console.WriteLine($"Warmup: {WarmupSeconds}s Measure: {MeasureSeconds}s");
Console.WriteLine();
// Warmup
Console.Write("Warming up... ");
RunLoop(store, Rql, WarmupSeconds, out _, out _);
Console.WriteLine("done");
// Measured run
Console.Write("Measuring... ");
RunLoop(store, Rql, MeasureSeconds, out long iterations, out double elapsed);
Console.WriteLine("done");
Console.WriteLine();
double qps = iterations / elapsed;
double avgMs = elapsed * 1000.0 / iterations;
Console.WriteLine($" Iterations : {iterations:N0}");
Console.WriteLine($" Elapsed : {elapsed:F2} s");
Console.WriteLine($" Throughput : {qps:F0} queries/sec");
Console.WriteLine($" Avg latency: {avgMs:F3} ms/query (end-to-end including HTTP)");
Console.WriteLine();
// -----------------------------------------------------------------------
// Server-side Only Benchmark (Timings API)
// -----------------------------------------------------------------------
Console.WriteLine("=== Server-Side Only Benchmark (10s, using Timings API) ===");
var serverOnly = new List<double>();
var ssSw = Stopwatch.StartNew();
while (ssSw.Elapsed.TotalSeconds < MeasureSeconds)
{
using (var session = store.OpenSession(new SessionOptions { NoCaching = true }))
{
QueryTimings t = null;
_ = session.Advanced.DocumentQuery<object>("BenchIndex")
.Timings(out t)
.WhereEquals("Name", "1")
.AndAlso()
.WhereGreaterThan("Count", (long)100)
.OrderBy("Count", OrderingType.Long)
.ToList();
if (t != null) serverOnly.Add(t.DurationInMs);
}
}
ssSw.Stop();
if (serverOnly.Count > 0)
{
serverOnly.Sort();
int count = serverOnly.Count;
Console.WriteLine($" Queries : {count:N0}");
Console.WriteLine($" Avg : {serverOnly.Average():F3} ms");
Console.WriteLine($" Median : {serverOnly[count / 2]:F3} ms");
Console.WriteLine($" P95 : {serverOnly[(int)(count * 0.95)]:F3} ms");
Console.WriteLine($" P99 : {serverOnly[(int)(count * 0.99)]:F3} ms");
Console.WriteLine($" Min : {serverOnly.Min():F3} ms");
Console.WriteLine($" Max : {serverOnly.Max():F3} ms");
}
Console.WriteLine();
// -----------------------------------------------------------------------
// Cleanup
// -----------------------------------------------------------------------
store.Maintenance.Server.Send(new DeleteDatabasesOperation(DatabaseName, hardDelete: true));
Console.WriteLine("Database cleaned up. Done.");
}
finally
{
store.Dispose();
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
void RunLoop(IDocumentStore s, string rql, int seconds, out long iters, out double elapsedSec)
{
var sw = Stopwatch.StartNew();
long count = 0;
var deadline = TimeSpan.FromSeconds(seconds);
while (sw.Elapsed < deadline)
{
using (var session = s.OpenSession(new SessionOptions { NoCaching = true }))
{
_ = session.Advanced.RawQuery<object>(rql).ToList();
}
count++;
}
sw.Stop();
iters = count;
elapsedSec = sw.Elapsed.TotalSeconds;
}
void PrintPlan(QueryInspectionNode node, int indent)
{
Console.WriteLine($"{new string(' ', indent * 2)}{node.Operation}");
if (node.Children != null)
foreach (var child in node.Children)
PrintPlan(child, indent + 1);
}
bool FindNode(QueryInspectionNode node, string name, int depth = 0)
{
if (depth > 20) return false;
if (node.Operation != null && node.Operation.Contains(name)) return true;
if (node.Children != null)
foreach (var child in node.Children)
if (FindNode(child, name, depth + 1)) return true;
return false;
}
public class Doc
{
public string Id { get; set; }
public string Name { get; set; }
public int Count { get; set; }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment