📊 50 Million Query Benchmark
Prepared statements • Pipeline mode • 10,000 queries per batch
| Driver | Protocol | Queries/Second | vs Rust |
|---|---|---|---|
| Rust qail-pg | 331,885 🏆 | 100% | |
| Zig (prepared) | 315,708 | 95% | |
| Zig (simple query) | 92,000 | 28% |
🦎 Only 5% slower than native Rust!
Zig's native I/O + Rust encoding = best of both worlds
⚡ Fair Comparison: QAIL vs pg.zig
Single query mode • Both parse responses • Apples-to-apples
📖 Why This Comparison Matters
pg.zig by @karlseguin is a pure Zig PostgreSQL driver using the Extended Query Protocol. We compared single-query performance to test raw driver speed.
- Initial test: QAIL-Zig was 2.1x faster... but we weren't parsing responses!
- We built: Response parsing FFI (qail_decode_response, qail_response_get_*)
- Linker fix: Static lib (119MB) crashed Zig → Dynamic lib (1.7MB) works!
| Driver | Work Done | Queries/Second | Rows Parsed |
|---|---|---|---|
| QAIL-Zig | | 33,866 🏆 | 55,000 |
| pg.zig | | 16,990 | 55,000 |
🔧 Response Parsing FFI (Built for this benchmark)
New Functions Added:
qail_decode_response(bytes) → handle
qail_response_row_count(handle) → usize
qail_response_get_i32(handle, row, col)
qail_response_get_string(handle, row, col)
qail_response_free(handle)
Linker Challenge Solved:
• Static library: 119MB → Zig linker
segfault 💀
• Dynamic library: 1.7MB → Works
perfectly! ✅
• Feature flag: --features response
🔥 QAIL-Zig is 2.0x faster than pg.zig!
Both parse 55,000 rows • Same work • Fair comparison
Why is QAIL Still 2x Faster?
vs 4 msgs (Extended)
Battle-tested qail-pg
Buffer reuse ftw
🏗️ Architecture
┌─────────────────────────────────────────────────────────────────┐ │ qail-zig (Zig native I/O) │ │ ├── std.net.tcpConnectToAddress() ← Native Zig TCP │ │ ├── stream.write(bytes) ← Zero-copy send │ │ └── stream.read(response) ← Native receive │ ├─────────────────────────────────────────────────────────────────┤ │ qail-encoder (Rust FFI) ~60MB static lib │ │ ├── qail_encode_parse() ← Prepare statement │ │ ├── qail_encode_bind_execute_batch() ← Pipeline encoding │ │ └── qail_encode_sync() ← Wait for response │ ├─────────────────────────────────────────────────────────────────┤ │ PostgreSQL Wire Protocol │ └─────────────────────────────────────────────────────────────────┘
💡 Key Findings
- Prepared statements matter: 315K vs 92K q/s (3.4x difference)
- Wire bytes: 30 bytes/query (prepared) vs 42 bytes/query (simple)
- FFI overhead: ~1M calls/sec, negligible vs I/O
- Memory: Zero allocations in hot path (both Rust and Zig)