I did play around with my queries locally and noticed that when I didn't fill my nullable string columns and afterwards selecting them, that the parsing time increases per each column by a lot.
After creating a benchmark test I noticed that this was only the case when wrapping the result set inside a tuple. I will post the benchmark code here and also the results.
=== Tuple Comparison: SET vs NOT SET ===
SET - Average: 3.8496 ms, Min: 3.6300 ms, Max: 4.0841 ms
NOT SET - Average: 10.8964 ms, Min: 9.6340 ms, Max: 34.6869 ms
Difference: -64.67%
NOT SET is 64.67% slower
Test benchmarkComparisonSetVsNotSet() passed after 0.868 seconds.
Test benchmarkComparisonSetVsNotSetNoTuple() started.
=== Comparison: SET vs NOT SET ===
SET - Average: 1.9919 ms, Min: 1.8420 ms, Max: 2.3141 ms
NOT SET - Average: 1.9110 ms, Min: 1.7960 ms, Max: 2.0641 ms
Difference: 4.23%
SET is 4.23% slower
Test benchmarkComparisonSetVsNotSetNoTuple() passed after 0.314 seconds.
Suite OptionalStringBenchmarkTests passed after 1.182 seconds.
Suite SnapshotTests passed after 1.182 seconds.
Test run with 2 tests in 2 suites passed after 1.183 seconds.
Program ended with exit code: 0
import Dependencies
import Foundation
import StructuredQueries
import Testing
import _StructuredQueriesSQLite
extension SnapshotTests {
@Suite struct OptionalStringBenchmarkTests {
@Dependency(\.defaultDatabase) var db
// Table with optional string values for benchmarking
@Table
struct BenchmarkTable: Hashable, Codable {
let id: Int
var name: String
var string1: String
var string2: String
var string3: String
var string4: String
var string5: String
var optionalString1: String?
var optionalString2: String?
var optionalString3: String?
var optionalString4: String?
var optionalString5: String?
}
init() throws {
// Create the benchmark table
try db.execute(
#sql(
"""
CREATE TABLE IF NOT EXISTS "benchmarkTables" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"string1" TEXT NOT NULL,
"string2" TEXT NOT NULL,
"string3" TEXT NOT NULL,
"string4" TEXT NOT NULL,
"string5" TEXT NOT NULL,
"optionalString1" TEXT,
"optionalString2" TEXT,
"optionalString3" TEXT,
"optionalString4" TEXT,
"optionalString5" TEXT
)
"""
)
)
// Clear existing data
try db.execute(BenchmarkTable.delete())
// Insert data with optional strings SET (1000 rows)
let valuesSet: [BenchmarkTable] = (1...3000).map { i in
BenchmarkTable(
id: i,
name: "Item \(i)",
string1: "string1 \(i)",
string2: "string2 \(i)",
string3: "string3 \(i)",
string4: "string4 \(i)",
string5: "string5 \(i)",
optionalString1: "Value1-\(i)",
optionalString2: "Value2-\(i)",
optionalString3: "Value3-\(i)",
optionalString4: "Value4-\(i)",
optionalString5: "Value5-\(i)",
)
}
try db.execute(BenchmarkTable.insert(valuesSet))
// Insert data with optional strings NOT SET (1000 rows)
let valuesNotSet: [BenchmarkTable] = (3001...6000).map { i in
BenchmarkTable(
id: i,
name: "Item \(i)",
string1: "string1 \(i)",
string2: "string2 \(i)",
string3: "string3 \(i)",
string4: "string4 \(i)",
string5: "string5 \(i)",
optionalString1: "Value1-\(i)",
optionalString2: "Value2-\(i)",
optionalString4: "Value4-\(i)",
)
}
try db.execute(BenchmarkTable.insert(valuesNotSet))
}
func measureExecutionTime<each V: QueryRepresentable>(
_ query: some StructuredQueriesCore.Statement<(repeat each V)>,
iterations: Int = 10
) -> (averageTime: TimeInterval, minTime: TimeInterval, maxTime: TimeInterval) {
var times: [TimeInterval] = []
for _ in 0..<iterations {
let startTime = Date()
_ = try? db.execute(query)
let endTime = Date()
let executionTime = endTime.timeIntervalSince(startTime)
times.append(executionTime)
}
let average = times.reduce(0, +) / Double(times.count)
let min = times.min() ?? 0
let max = times.max() ?? 0
return (averageTime: average, minTime: min, maxTime: max)
}
@Test func benchmarkComparisonSetVsNotSet() throws {
let querySet = BenchmarkTable.all
.where { $0.id <= 3000 }
.select { ($0.id, $0.name, $0.string1, $0.string2, $0.string3, $0.string4, $0.string5, $0.optionalString1, $0.optionalString2, $0.optionalString3, $0.optionalString4, $0.optionalString5) }
let queryNotSet = BenchmarkTable.all
.where { $0.id > 3000 }
.select { ($0.id, $0.name, $0.string1, $0.string2, $0.string3, $0.string4, $0.string5, $0.optionalString1, $0.optionalString2, $0.optionalString3, $0.optionalString4, $0.optionalString5) }
print("\n=== Tuple Comparison: SET vs NOT SET ===")
benchmarkTwoQueries(querySet, queryNotSet)
}
@Test func benchmarkComparisonSetVsNotSetNoTuple() throws {
let querySet = BenchmarkTable.all
.where { $0.id <= 3000 }
.select { $0 }
let queryNotSet = BenchmarkTable.all
.where { $0.id > 3000 }
.select { $0 }
print("\n=== Comparison: SET vs NOT SET ===")
benchmarkTwoQueries(querySet, queryNotSet)
}
func benchmarkTwoQueries<each V: QueryRepresentable>(_ querySet: some StructuredQueriesCore.Statement<(repeat each V)>,
_ queryNotSet: some StructuredQueriesCore.Statement<(repeat each V)>) {
let (avgSet, minSet, maxSet) = measureExecutionTime(querySet, iterations: 50)
let (avgNotSet, minNotSet, maxNotSet) = measureExecutionTime(queryNotSet, iterations: 50)
print("SET - Average: \(String(format: "%.4f", avgSet * 1000)) ms, Min: \(String(format: "%.4f", minSet * 1000)) ms, Max: \(String(format: "%.4f", maxSet * 1000)) ms")
print("NOT SET - Average: \(String(format: "%.4f", avgNotSet * 1000)) ms, Min: \(String(format: "%.4f", minNotSet * 1000)) ms, Max: \(String(format: "%.4f", maxNotSet * 1000)) ms")
let difference = ((avgSet - avgNotSet) / avgNotSet) * 100
print("Difference: \(String(format: "%.2f", difference))%")
if avgSet > avgNotSet {
print("SET is \(String(format: "%.2f", difference))% slower")
} else {
print("NOT SET is \(String(format: "%.2f", abs(difference)))% slower")
}
}
}
}
Description
Hello,
I did play around with my queries locally and noticed that when I didn't fill my nullable string columns and afterwards selecting them, that the parsing time increases per each column by a lot.
After creating a benchmark test I noticed that this was only the case when wrapping the result set inside a tuple. I will post the benchmark code here and also the results.
Benchmark results
Code
Checklist
mainbranch of this package.Expected behavior
No response
Actual behavior
No response
Reproducing project
No response
Structured Queries version information
34453b5
Destination operating system
macOS
Xcode version information
Version 26.2 (17C52)
Swift Compiler version information