#!/usr/bin/env ruby # frozen_string_literal: true require "bundler/inline" require "benchmark" require "tempfile" gemfile(true) do source "https://rubygems.org" gem "extralite-bundle", require: "extralite" gem "sqlite3" gem "duckdb" end ROW_COUNT = 50_000_000 SOME_DATA = ["The quick, brown fox jumps over a lazy dog.", 1_234_567_890] def with_db_path tempfile = Tempfile.new yield tempfile.path ensure tempfile.close tempfile.unlink end module Sqlite TRANSACTION_SIZE = 1000 CREATE_TABLE_SQL = <<~SQL CREATE TABLE foo ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, some_text TEXT, some_number INTEGER ) SQL INSERT_SQL = "INSERT INTO foo (some_text, some_number) VALUES (?, ?)" class Sqlite3Benchmark def initialize(row_count) @row_count = row_count @tempfile = Tempfile.new @connection = SQLite3::Database.new(@tempfile.path) @connection.journal_mode = "wal" @connection.synchronous = "off" @connection.temp_store = "memory" @connection.locking_mode = "normal" @connection.cache_size = -10_000 # 10_000 pages @connection.execute(CREATE_TABLE_SQL) @stmt = @connection.prepare(INSERT_SQL) @statement_counter = 0 end def run @row_count.times { insert(SOME_DATA) } close end private def insert(*parameters) begin_transaction if @statement_counter == 0 @stmt.execute(*parameters) if (@statement_counter += 1) > TRANSACTION_SIZE commit_transaction @statement_counter = 0 end end def begin_transaction return if @connection.transaction_active? @connection.transaction(:deferred) end def commit_transaction return unless @connection.transaction_active? @connection.commit end def close commit_transaction @stmt.close @connection.close @tempfile.close @tempfile.unlink end end class ExtraliteBenchmark def initialize(row_count) @row_count = row_count @tempfile = Tempfile.new @connection = Extralite::Database.new(@tempfile.path) @connection.pragma( journal_mode: "wal", synchronous: "off", temp_store: "memory", locking_mode: "normal", cache_size: -10_000, # 10_000 pages ) @connection.execute(CREATE_TABLE_SQL) @stmt = @connection.prepare(INSERT_SQL) @statement_counter = 0 end def run @row_count.times { insert(SOME_DATA) } close end private def insert(*parameters) begin_transaction if @statement_counter == 0 @stmt.execute(*parameters) if (@statement_counter += 1) > TRANSACTION_SIZE commit_transaction @statement_counter = 0 end end def begin_transaction return if @connection.transaction_active? @connection.execute("BEGIN DEFERRED TRANSACTION") end def commit_transaction return unless @connection.transaction_active? @connection.execute("COMMIT") end def close commit_transaction @stmt.close @connection.close @tempfile.close @tempfile.unlink end end end class DuckDbBenchmark CREATE_TABLE_SQL = <<~SQL CREATE TABLE foo ( id INTEGER NOT NULL PRIMARY KEY, some_text TEXT, some_number INTEGER ) SQL def initialize(row_count) @row_count = row_count @tempfile = Tempfile.new FileUtils.rm(@tempfile.path) @db = DuckDB::Database.open(@tempfile.path) @connection = @db.connect @connection.query(CREATE_TABLE_SQL) @appender = @connection.appender("foo") end def run @row_count.times do |id| @appender.begin_row @appender.append(id) @appender.append(SOME_DATA[0]) @appender.append(SOME_DATA[1]) @appender.end_row end close end private def close @appender.close @connection.close @db.close end end Benchmark.bm(15) do |x| x.report("SQLite3") { Sqlite::Sqlite3Benchmark.new(ROW_COUNT).run } x.report("Extralite") { Sqlite::ExtraliteBenchmark.new(ROW_COUNT).run } x.report("DuckDB") { DuckDbBenchmark.new(ROW_COUNT).run } end