Skip to content

Commit 4e987e9

Browse files
author
MPCoreDeveloper
committed
in between status fase 2
1 parent ce477d2 commit 4e987e9

File tree

7 files changed

+243
-31
lines changed

7 files changed

+243
-31
lines changed

SharpCoreDB.Benchmarks/PerformanceTest.cs

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public static void RunPerformanceTest()
3030
DisplayComparison("Insert 10k records", sharpResults.InsertTime, sqliteResults.InsertTime);
3131
DisplayComparison("Select with WHERE", sharpResults.SelectTime, sqliteResults.SelectTime);
3232
DisplayComparison("Select 1000 records", sharpResults.SelectMultipleTime, sqliteResults.SelectMultipleTime);
33+
DisplayComparison("1000 Indexed SELECTs", sharpResults.IndexedSelectTotalMs, sqliteResults.IndexedSelectTime);
3334

3435
Console.WriteLine();
3536
// === AUTO-GENERATE README TABLE ===
@@ -39,7 +40,7 @@ public static void RunPerformanceTest()
3940
Console.WriteLine("| Operation | SharpCoreDB | SQLite | Winnaar |");
4041
Console.WriteLine("|-------------------------------------------|-------------------|-----------------|--------------------------|");
4142
Console.WriteLine($"| Insert 10,000 records | **{sharpResults.InsertTime:F0} ms** | {sqliteResults.InsertTime:F0} ms | **SharpCoreDB ×{sqliteResults.InsertTime / sharpResults.InsertTime:F1}** |");
42-
Console.WriteLine($"| 1,000 × Indexed SELECT (WHERE = value) | **{sharpResults.IndexedSelectTotalMs:F1} ms** | ~900 ms | **SharpCoreDB ×~158** |");
43+
Console.WriteLine($"| 1,000 × Indexed SELECT (WHERE = value) | **{sharpResults.IndexedSelectTotalMs:F1} ms** | {sqliteResults.IndexedSelectTime:F1} ms | {(sharpResults.IndexedSelectTotalMs < sqliteResults.IndexedSelectTime ? "SharpCoreDB" : "SQLite")} |");
4344
Console.WriteLine($"| Full table scan (1000 records) | {sharpResults.SelectMultipleTime:F0} ms | {sqliteResults.SelectMultipleTime:F0} ms | {(sharpResults.SelectMultipleTime < sqliteResults.SelectMultipleTime ? "SharpCoreDB" : "SQLite")} |");
4445
Console.WriteLine();
4546
Console.WriteLine("> Pure .NET 10 • Zero native deps • Run locally for your hardware");
@@ -98,14 +99,23 @@ private static (double InsertTime, double SelectTime, double SelectMultipleTime,
9899
for (int i = 0; i < recordCount; i++) testBatch.Add($"INSERT INTO Test VALUES ({i}, 'Test{i}')");
99100
db.ExecuteBatchSQL(testBatch);
100101

101-
var swIndexed = Stopwatch.StartNew();
102+
// Without index
103+
var swWithout = Stopwatch.StartNew();
102104
for (int i = 0; i < 1000; i++) db.ExecuteSQL("SELECT * FROM Test WHERE Id = 5000");
103-
swIndexed.Stop();
105+
swWithout.Stop();
106+
Console.WriteLine($"1000 SELECTs without index: {swWithout.Elapsed.TotalMilliseconds:F1} ms ({swWithout.Elapsed.TotalMilliseconds / 1000:F3} ms/query)");
104107

105-
double avg = swIndexed.Elapsed.TotalMilliseconds / 1000;
106-
Console.WriteLine($"1000 SELECTs took {swIndexed.ElapsedMilliseconds:F1} ms ? {avg:F3} ms/query");
108+
// With index
109+
db.ExecuteSQL("CREATE INDEX idx_id ON Test (Id)");
110+
var swWith = Stopwatch.StartNew();
111+
for (int i = 0; i < 1000; i++) db.ExecuteSQL("SELECT * FROM Test WHERE Id = 5000");
112+
swWith.Stop();
113+
Console.WriteLine($"1000 SELECTs with index: {swWith.Elapsed.TotalMilliseconds:F1} ms ({swWith.Elapsed.TotalMilliseconds / 1000:F3} ms/query)");
114+
115+
var speedup = swWithout.Elapsed.TotalMilliseconds / swWith.Elapsed.TotalMilliseconds;
116+
Console.WriteLine($"Speedup with index: {speedup:F1}x");
107117

108-
return (insertTime, selectTime, selectMultipleTime, swIndexed.Elapsed.TotalMilliseconds);
118+
return (insertTime, selectTime, selectMultipleTime, swWith.Elapsed.TotalMilliseconds);
109119
}
110120
finally
111121
{
@@ -114,7 +124,7 @@ private static (double InsertTime, double SelectTime, double SelectMultipleTime,
114124
}
115125
}
116126

117-
private static (double InsertTime, double SelectTime, double SelectMultipleTime) TestSQLite(int recordCount)
127+
private static (double InsertTime, double SelectTime, double SelectMultipleTime, double IndexedSelectTime) TestSQLite(int recordCount)
118128
{
119129
var dbPath = Path.Combine(Path.GetTempPath(), $"perf_test_sqlite_{Guid.NewGuid()}.db");
120130

@@ -166,7 +176,20 @@ private static (double InsertTime, double SelectTime, double SelectMultipleTime)
166176
var selectMultipleTime = sw.Elapsed.TotalMilliseconds;
167177
Console.WriteLine($"SQLite Select Multiple: {selectMultipleTime:F0}ms");
168178

169-
return (insertTime, selectTime, selectMultipleTime);
179+
// Indexed SELECT benchmark (1000 lookups)
180+
sw.Restart();
181+
for (int i = 0; i < 1000; i++)
182+
{
183+
using var cmd = conn.CreateCommand();
184+
cmd.CommandText = "SELECT * FROM time_entries WHERE id = 5000";
185+
using var reader = cmd.ExecuteReader();
186+
while (reader.Read()) { }
187+
}
188+
sw.Stop();
189+
var indexedSelectTime = sw.Elapsed.TotalMilliseconds;
190+
Console.WriteLine($"SQLite 1000 Indexed SELECTs: {indexedSelectTime:F1} ms");
191+
192+
return (insertTime, selectTime, selectMultipleTime, indexedSelectTime);
170193
}
171194
finally
172195
{

SharpCoreDB.Tests/QueryCacheTests.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,4 +165,36 @@ public void QueryCache_CacheSizeLimit_EvictsLeastUsed()
165165
// Cleanup
166166
Directory.Delete(_testDbPath, true);
167167
}
168+
169+
[Fact]
170+
public void QueryCache_ParameterizedQueries_ImprovesHitRate()
171+
{
172+
// Arrange
173+
var factory = _serviceProvider.GetRequiredService<DatabaseFactory>();
174+
var config = new DatabaseConfig { EnableQueryCache = true, QueryCacheSize = 1024 };
175+
var db = factory.Create(_testDbPath, "test123", false, config);
176+
177+
db.ExecuteSQL("CREATE TABLE users (id INTEGER, name TEXT)");
178+
for (int i = 0; i < 100; i++)
179+
{
180+
db.ExecuteSQL($"INSERT INTO users VALUES ({i}, 'User{i}')");
181+
}
182+
183+
// Act - Execute same parameterized SELECT query multiple times with different @id
184+
var sql = "SELECT * FROM users WHERE id = ?";
185+
for (int i = 0; i < 50; i++)
186+
{
187+
db.ExecuteSQL(sql, new Dictionary<string, object?> { ["0"] = i % 100 });
188+
}
189+
190+
// Assert
191+
var stats = db.GetQueryCacheStatistics();
192+
Assert.True(stats.Hits > 0, "Cache should have hits from repeated parameterized queries");
193+
// With 1 CREATE + 100 INSERTs + 50 SELECTs (49 repeated) = 151 total queries
194+
// Cache should improve hit rate for repeated parameterized queries
195+
Assert.True(stats.HitRate > 0.3, $"Hit rate should be >30% for repeated parameterized queries, got {stats.HitRate:P2}");
196+
197+
// Cleanup
198+
Directory.Delete(_testDbPath, true);
199+
}
168200
}

SharpCoreDB/DatabaseConfig.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public class DatabaseConfig
2424
/// <summary>
2525
/// Gets the query cache size limit.
2626
/// </summary>
27-
public int QueryCacheSize { get; init; } = 1000;
27+
public int QueryCacheSize { get; init; } = 1024;
2828

2929
/// <summary>
3030
/// Gets the WAL buffer size in bytes.

SharpCoreDB/Directory.Build.props

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
<PropertyGroup>
33
<Nullable>enable</Nullable>
44
<LangVersion>14</LangVersion>
5+
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
56
</PropertyGroup>
6-
<ItemGroup>
7-
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" PrivateAssets="All" />
8-
</ItemGroup>
97
</Project>

SharpCoreDB/Services/SqlParser.cs

Lines changed: 111 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,10 @@ public void Execute(string sql, IWAL? wal = null)
4444
/// <inheritdoc />
4545
public void Execute(string sql, Dictionary<string, object?> parameters, IWAL? wal = null)
4646
{
47-
// If parameters are provided, bind them to ? placeholders
47+
string? originalSql = null;
4848
if (parameters != null && parameters.Count > 0)
4949
{
50+
originalSql = sql;
5051
sql = this.BindParameters(sql, parameters);
5152
}
5253
else
@@ -59,7 +60,7 @@ public void Execute(string sql, Dictionary<string, object?> parameters, IWAL? wa
5960

6061
// Proceed with existing logic
6162
var parts = sql.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries);
62-
this.ExecuteInternal(sql, parts, wal);
63+
this.ExecuteInternal(originalSql ?? sql, parts, wal, originalSql ?? sql);
6364
}
6465

6566
/// <summary>
@@ -80,7 +81,7 @@ public void Execute(CachedQueryPlan plan, Dictionary<string, object?> parameters
8081
sql = this.SanitizeSql(sql);
8182
}
8283

83-
this.ExecuteInternal(sql, plan.Parts, wal);
84+
this.ExecuteInternal(sql, plan.Parts, wal, plan.Sql);
8485
}
8586

8687
/// <summary>
@@ -104,7 +105,7 @@ public List<Dictionary<string, object>> ExecuteQuery(string sql, Dictionary<stri
104105
return this.ExecuteQueryInternal(sql, parts);
105106
}
106107

107-
private void ExecuteInternal(string sql, string[] parts, IWAL? wal = null)
108+
private void ExecuteInternal(string sql, string[] parts, IWAL? wal = null, string? cacheKey = null)
108109
{
109110
// Parts are already provided, no need to parse again
110111

@@ -165,6 +166,19 @@ private void ExecuteInternal(string sql, string[] parts, IWAL? wal = null)
165166
};
166167
this.tables[tableName] = table;
167168
wal?.Log(sql);
169+
// Auto-create hash index on primary key for faster lookups
170+
if (primaryKeyIndex >= 0)
171+
{
172+
table.CreateHashIndex(table.Columns[primaryKeyIndex]);
173+
}
174+
// Auto-create hash indexes on all columns for faster WHERE lookups
175+
for (int i = 0; i < columns.Count; i++)
176+
{
177+
if (i != primaryKeyIndex)
178+
{
179+
table.CreateHashIndex(columns[i]);
180+
}
181+
}
168182
}
169183
else if (parts[0].ToUpper() == SqlConstants.CREATE && parts[1].ToUpper() == "INDEX")
170184
{
@@ -268,6 +282,52 @@ private void ExecuteInternal(string sql, string[] parts, IWAL? wal = null)
268282
this.tables[tableName].Insert(row);
269283
wal?.Log(sql);
270284
}
285+
else if (parts[0].ToUpper() == "EXPLAIN")
286+
{
287+
if (parts.Length < 2 || parts[1].ToUpper() != "SELECT")
288+
{
289+
throw new InvalidOperationException("EXPLAIN only supports SELECT queries");
290+
}
291+
var selectParts = parts.Skip(1).ToArray();
292+
var fromIdx = Array.IndexOf(selectParts, SqlConstants.FROM);
293+
if (fromIdx < 0)
294+
{
295+
throw new InvalidOperationException("Invalid SELECT query for EXPLAIN");
296+
}
297+
var tableName = selectParts[fromIdx + 1];
298+
if (!this.tables.ContainsKey(tableName))
299+
{
300+
throw new InvalidOperationException($"Table {tableName} does not exist");
301+
}
302+
var whereIdx = Array.IndexOf(selectParts, SqlConstants.WHERE);
303+
string plan = "Full table scan";
304+
if (whereIdx > 0)
305+
{
306+
var whereStr = string.Join(" ", selectParts.Skip(whereIdx + 1));
307+
var whereTokens = whereStr.Split(' ', StringSplitOptions.RemoveEmptyEntries);
308+
if (whereTokens.Length >= 3 && whereTokens[1] == "=")
309+
{
310+
var col = whereTokens[0];
311+
if (this.tables[tableName].HasHashIndex(col))
312+
{
313+
plan = $"Hash index lookup on {col}";
314+
}
315+
else if (this.tables[tableName].PrimaryKeyIndex >= 0 && this.tables[tableName].Columns[this.tables[tableName].PrimaryKeyIndex] == col)
316+
{
317+
plan = $"Primary key lookup on {col}";
318+
}
319+
else
320+
{
321+
plan = $"Full table scan with WHERE on {col}";
322+
}
323+
}
324+
else
325+
{
326+
plan = "Full table scan with complex WHERE";
327+
}
328+
}
329+
Console.WriteLine($"Query Plan: {plan}");
330+
}
271331
else if (parts[0].ToUpper() == SqlConstants.SELECT)
272332
{
273333
var fromIdx = Array.IndexOf(parts, SqlConstants.FROM);
@@ -344,10 +404,10 @@ private void ExecuteInternal(string sql, string[] parts, IWAL? wal = null)
344404
var results = new List<Dictionary<string, object>>();
345405
foreach (var r1 in rows1)
346406
{
347-
var key = r1[left];
348-
if (dict2.ContainsKey(key ?? new object()))
407+
var joinKey = r1[left];
408+
if (dict2.ContainsKey(joinKey ?? new object()))
349409
{
350-
foreach (var r2 in dict2[key ?? new object()])
410+
foreach (var r2 in dict2[joinKey ?? new object()])
351411
{
352412
var combined = new Dictionary<string, object>();
353413
foreach (var kv in r1)
@@ -416,6 +476,7 @@ private void ExecuteInternal(string sql, string[] parts, IWAL? wal = null)
416476
else
417477
{
418478
var tableName = fromParts[0];
479+
Console.WriteLine($"Query Plan: {GetQueryPlan(tableName, whereStr)}");
419480
var results = this.tables[tableName].Select(whereStr, orderBy, asc);
420481

421482
// Apply limit and offset
@@ -541,14 +602,14 @@ private bool EvaluateJoinWhere(Dictionary<string, object> row, string where)
541602

542603
return op switch
543604
{
544-
"=" => (rowValue?.ToString() == value),
545-
"!=" => (rowValue?.ToString() != value),
605+
"=" => rowValue?.ToString() == value,
606+
"!=" => rowValue?.ToString() != value,
546607
"<" => Comparer<object>.Default.Compare(rowValue, value) < 0,
547608
"<=" => Comparer<object>.Default.Compare(rowValue, value) <= 0,
548609
">" => Comparer<object>.Default.Compare(rowValue, value) > 0,
549610
">=" => Comparer<object>.Default.Compare(rowValue, value) >= 0,
550-
"LIKE" => rowValue?.ToString().Contains(value.Replace("%", "").Replace("_", "")) == true,
551-
"NOT LIKE" => rowValue?.ToString().Contains(value.Replace("%", "").Replace("_", "")) != true,
611+
"LIKE" => rowValue?.ToString().Contains(value.Replace("%", string.Empty).Replace("_", string.Empty)) == true,
612+
"NOT LIKE" => rowValue?.ToString().Contains(value.Replace("%", string.Empty).Replace("_", string.Empty)) != true,
552613
"IN" => value.Split(',').Select(v => v.Trim().Trim('\'')).Contains(rowValue?.ToString()),
553614
"NOT IN" => !value.Split(',').Select(v => v.Trim().Trim('\'')).Contains(rowValue?.ToString()),
554615
_ => throw new InvalidOperationException($"Unsupported operator {op}"),
@@ -579,14 +640,14 @@ private bool EvaluateJoinWhere(Dictionary<string, object> row, string where)
579640

580641
subConditions.Add(op switch
581642
{
582-
"=" => (rowValue?.ToString() == value),
583-
"!=" => (rowValue?.ToString() != value),
643+
"=" => rowValue?.ToString() == value,
644+
"!=" => rowValue?.ToString() != value,
584645
"<" => Comparer<object>.Default.Compare(rowValue, value) < 0,
585646
"<=" => Comparer<object>.Default.Compare(rowValue, value) <= 0,
586647
">" => Comparer<object>.Default.Compare(rowValue, value) > 0,
587648
">=" => Comparer<object>.Default.Compare(rowValue, value) >= 0,
588-
"LIKE" => rowValue?.ToString().Contains(value.Replace("%", "").Replace("_", "")) == true,
589-
"NOT LIKE" => rowValue?.ToString().Contains(value.Replace("%", "").Replace("_", "")) != true,
649+
"LIKE" => rowValue?.ToString().Contains(value.Replace("%", string.Empty).Replace("_", string.Empty)) == true,
650+
"NOT LIKE" => rowValue?.ToString().Contains(value.Replace("%", string.Empty).Replace("_", string.Empty)) != true,
590651
"IN" => value.Split(',').Select(v => v.Trim().Trim('\'')).Contains(rowValue?.ToString()),
591652
"NOT IN" => !value.Split(',').Select(v => v.Trim().Trim('\'')).Contains(rowValue?.ToString()),
592653
_ => throw new InvalidOperationException($"Unsupported operator {op}"),
@@ -607,7 +668,7 @@ private string BindParameters(string sql, Dictionary<string, object?> parameters
607668
foreach (var param in parameters)
608669
{
609670
var paramName = param.Key;
610-
var valueStr = FormatValue(param.Value);
671+
var valueStr = this.FormatValue(param.Value);
611672

612673
// Try matching with @ prefix
613674
if (paramName.StartsWith("@"))
@@ -755,10 +816,10 @@ private List<Dictionary<string, object>> ExecuteQueryInternal(string sql, string
755816
var results = new List<Dictionary<string, object>>();
756817
foreach (var r1 in rows1)
757818
{
758-
var key = r1[left];
759-
if (dict2.ContainsKey(key ?? new object()))
819+
var joinKey = r1[left];
820+
if (dict2.ContainsKey(joinKey ?? new object()))
760821
{
761-
foreach (var r2 in dict2[key ?? new object()])
822+
foreach (var r2 in dict2[joinKey ?? new object()])
762823
{
763824
var combined = new Dictionary<string, object>();
764825
foreach (var kv in r1)
@@ -824,6 +885,7 @@ private List<Dictionary<string, object>> ExecuteQueryInternal(string sql, string
824885
else
825886
{
826887
var tableName = fromParts[0];
888+
Console.WriteLine($"Query Plan: {GetQueryPlan(tableName, whereStr)}");
827889
var results = this.tables[tableName].Select(whereStr, orderBy, asc);
828890

829891
// Apply limit and offset
@@ -846,4 +908,34 @@ private List<Dictionary<string, object>> ExecuteQueryInternal(string sql, string
846908
return new List<Dictionary<string, object>>();
847909
}
848910
}
911+
912+
private string GetQueryPlan(string tableName, string? whereStr)
913+
{
914+
string plan = "Full table scan";
915+
if (!string.IsNullOrEmpty(whereStr))
916+
{
917+
var whereTokens = whereStr.Split(' ', StringSplitOptions.RemoveEmptyEntries);
918+
if (whereTokens.Length >= 3 && whereTokens[1] == "=")
919+
{
920+
var col = whereTokens[0];
921+
if (this.tables[tableName].HasHashIndex(col))
922+
{
923+
plan = $"Hash index lookup on {col}";
924+
}
925+
else if (this.tables[tableName].PrimaryKeyIndex >= 0 && this.tables[tableName].Columns[this.tables[tableName].PrimaryKeyIndex] == col)
926+
{
927+
plan = $"Primary key lookup on {col}";
928+
}
929+
else
930+
{
931+
plan = $"Full table scan with WHERE on {col}";
932+
}
933+
}
934+
else
935+
{
936+
plan = "Full table scan with complex WHERE";
937+
}
938+
}
939+
return plan;
940+
}
849941
}

0 commit comments

Comments
 (0)