Skip to content

Not filled Optional Values inside Tuple Select increases parsing time #246

Description

@Seyden

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

=== 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

Code

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")
            }
        }
    }
}

Checklist

  • I have determined whether this bug is also reproducible in a vanilla SwiftUI project.
  • If possible, I've reproduced the issue using the main branch of this package.
  • This issue hasn't been addressed in an existing GitHub issue or discussion.

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

swift-driver version: 1.127.5.3 Apple Swift version 6.2.3 (swiftlang-6.2.3.3.21 clang-1700.6.3.2)
Target: arm64-apple-macosx15.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    apple bugSomething isn't working due to a language/framework bug

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions