PostgreSQL vs. MariaDB vs. SQLite: Ein Performance-Test

Welche Datenbank ist die beste für deine Anwendung? Ein detaillierter Performance-Benchmark-Vergleich zwischen PostgreSQL, MariaDB und SQLite.

PostgreSQL vs. MariaDB vs. SQLite: Ein Performance-Test hero image

Wenn man eine selbstgehostete Anwendung aufsetzt, stößt man unweigerlich auf eine entscheidende Frage: Welche Datenbank soll ich verwenden? So bietet beispielsweise Gitea laut der Dokumentation mehrere Optionen an, darunter PostgreSQL, MySQL/MariaDB und sogar das einfache SQLite. Doch welche ist die richtige für meinen Anwendungsfall? Ist eine ausgewachsene Client-Server-Datenbank wie PostgreSQL wirklich nötig, oder reicht das leichtgewichtige SQLite vielleicht sogar aus?

Um diese Frage zu klären, habe ich ein Experiment gestartet: Ich habe drei absolut identische Server aufgesetzt und auf jedem Gitea mit einer anderen Datenbank installiert. Anschließend habe ich sie mit einem rohen Benchmark-Tests geprüft. Die Ergebnisse waren teilweise überraschend.

Die Testumgebung: Drei identische Server für einen fairen Kampf

Um einen fairen Vergleich zu gewährleisten, benötige ich eine identische Hardware-Basis. Ich habe mich für drei kleine Cloud-Server (Modell CX22) bei Hetzner entschieden.

Transparenzhinweis: Wenn du über meinen Affiliate-Link bei Hetzner einen neuen Account erstellst, erhältst du 20 € Startguthaben und ich eine kleine Provision.

Beim Erstellen der Server habe ich darauf geachtet, dass alle am selben Standort (Nürnberg) sind und mit Ubuntu 24.04 laufen. Mehr zum Thema VPS für Anfänger und dessen Absicherung findest du in diesem Beitrag.

Nachdem die Server bereitgestellt waren (was einige Minuten dauern kann), habe ich mich per SSH mit jedem verbunden und als Erstes Docker installiert. Die Befehle dazu sind auf allen drei Servern identisch:

# Docker-Abhängigkeiten installieren
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Docker-Repository hinzufügen
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update

# Docker Engine installieren
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Drei Mal Gitea, Drei Datenbanken: Die Installation

Für jede Datenbank habe ich eine eigene docker-compose.yml erstellt. Ich habe mich dazu entschieden, für die Datenbanken eigene Ordner für die persistenten Daten zu erstellen und keine Docker Volumes zu nutzen.

Server 1: Gitea mit SQLite (Die Standard-Variante)

Auf dem ersten Server habe ich die einfachste Konfiguration aus der Gitea-Dokumentation verwendet, die auf SQLite als Datenbank setzt.

# Verzeichnisse erstellen
mkdir -p gitea
cd gitea
nano docker-compose.yml
# docker-compose.yml für SQLite
networks:
    gitea:
        external: false

services:
    server:
        image: gitea/gitea:1.23.8
        container_name: gitea
        environment:
            - USER_UID=1000
            - USER_GID=1000
        restart: always
        networks:
            - gitea
        volumes:
            - ./gitea:/data
            - /etc/timezone:/etc/timezone:ro
            - /etc/localtime:/etc/localtime:ro
        ports:
            - "3000:3000"
            - "222:22"

Server 2: Gitea mit MySQL

Auf dem zweiten Server habe ich die Konfiguration erweitert, um eine separate MySQL-Instanz zu nutzen.

# Verzeichnisse erstellen
mkdir -p gitea_mysql/{gitea,mysql}
cd gitea_mysql
nano docker-compose.yml
# docker-compose.yml für MySQL
version: "3"

networks:
    gitea:
        external: false

services:
    server:
        image: gitea/gitea:1.23.8
        container_name: gitea
        environment:
            - USER_UID=1000
            - USER_GID=1000
            - GITEA__database__DB_TYPE=mysql
            - GITEA__database__HOST=db:3306
            - GITEA__database__NAME=gitea
            - GITEA__database__USER=gitea
            - GITEA__database__PASSWD=gitea
        restart: always
        networks:
            - gitea
        volumes:
            - ./gitea:/data
            - /etc/timezone:/etc/timezone:ro
            - /etc/localtime:/etc/localtime:ro
        ports:
            - "3000:3000"
            - "222:22"
        depends_on:
            - db

    db:
        image: mysql:9
        restart: always
        environment:
            - MYSQL_ROOT_PASSWORD=gitea
            - MYSQL_USER=gitea
            - MYSQL_PASSWORD=gitea
            - MYSQL_DATABASE=gitea
        networks:
            - gitea
        volumes:
            - ./mysql:/var/lib/mysql

Server 3: Gitea mit PostgreSQL

Auf dem dritten Server kam eine PostgreSQL-Datenbank zum Einsatz.

# Verzeichnisse erstellen
mkdir -p gitea_postgres/{gitea,postgres}
cd gitea_postgres
nano docker-compose.yml
# docker-compose.yml für PostgreSQL
version: "3"

networks:
    gitea:
        external: false

services:
    server:
        image: gitea/gitea:1.23.8
        container_name: gitea
        environment:
            - USER_UID=1000
            - USER_GID=1000
            - GITEA__database__DB_TYPE=postgres
            - GITEA__database__HOST=db:5432
            - GITEA__database__NAME=gitea
            - GITEA__database__USER=gitea
            - GITEA__database__PASSWD=gitea
        restart: always
        networks:
            - gitea
        volumes:
            - ./gitea:/data
            - /etc/timezone:/etc/timezone:ro
            - /etc/localtime:/etc/localtime:ro
        ports:
            - "3000:3000"
            - "222:22"
        depends_on:
            - db

    db:
        image: postgres:14
        restart: always
        environment:
            - POSTGRES_USER=gitea
            - POSTGRES_PASSWORD=gitea
            - POSTGRES_DB=gitea
        networks:
            - gitea
        volumes:
            - ./postgres:/var/lib/postgresql/data

Nachdem alle drei Setups mit sudo docker compose up -d gestartet waren, zeigte ein erster Blick auf die Ressourcenauslastung (sudo docker stats) bereits Unterschiede: Der MariaDB-Container beanspruchte im Ruhezustand deutlich mehr RAM als die PostgreSQL- und die in Gitea integrierte SQLite-Lösung.

Anschließend habe ich auf jeder Instanz den Einrichtungsassistenten durchlaufen, einen Admin-Account erstellt und einen API-Token für die anstehenden Tests generiert.

Benchmark 1: Der API-Praxistest – Gitea im echten Einsatz

Um die Performance unter realistischen Bedingungen zu testen, habe ich ein Node.js-Skript geschrieben, das die Gitea-API nutzt, um typische Aktionen auszuführen. Ein vierter Server diente als Test-Client, um Netzwerkeffekte zu minimieren.

Das Skript führte folgende Operationen aus und maß die dafür benötigte Zeit:

  1. Repository-Erstellung (150 Repos): Eine schreibintensive Operation, die neue Einträge in mehreren Datenbanktabellen erzeugt.
  2. Issue-Erstellung (750 Issues): Simuliert viele kleine, aufeinanderfolgende Schreibvorgänge.
  3. Repository-Auflistung (150 Abrufe): Eine einfache Leseoperation.
  4. Issue-Auflistung (5 Abrufe): Eine komplexere Leseoperation, die wahrscheinlich Filter, Sortierungen und Joins erfordert.
const axios = require("axios");
const { performance } = require("perf_hooks");

const GITEA_INSTANCES = [
	{
		name: "Gitea-SQLite",
		baseUrl: "http:12.34.123.123:3000",
		apiToken: "4f1c9c59ada75d793e4c6dbfab884768e5d52fe4",
	},
	{
		name: "Gitea-MariaDB",
		baseUrl: "http://191.199.123.123:3000",
		apiToken: "ee077f0b0cc665445c97f0bbd5aa814740279871",
	},
	{
		name: "Gitea-PostgreSQL",
		baseUrl: "http://138.199.123.123:3000",
		apiToken: "1377c5d0f6e48ed1b93a716f0b51807f2ca010fc",
	},
];

const NUM_REPO_CREATIONS = 150;
const NUM_ISSUE_CREATIONS_PER_REPO = 5;
const NUM_REPO_LISTINGS = 150;
const NUM_ISSUE_LISTINGS_PER_REPO = 5;

function generateRandomString(length = 8) {
	return Math.random()
		.toString(36)
		.substring(2, 2 + length);
}

async function sleep(ms) {
	return new Promise((resolve) => setTimeout(resolve, ms));
}

// --- Gitea Testfunktionen ---

async function createRepository(instanceConfig, repoName) {
	const url = `${instanceConfig.baseUrl}/api/v1/user/repos`;
	const headers = { Authorization: `token ${instanceConfig.apiToken}` };
	const data = {
		name: repoName,
		private: false,
		description: `Test repository ${repoName}`,
	};
	try {
		const response = await axios.post(url, data, { headers });
		return response.data;
	} catch (error) {
		console.error(`[${instanceConfig.name}] Fehler beim Erstellen des Repos ${repoName}:`, error.response ? error.response.data : error.message);
		throw error;
	}
}

async function createIssue(instanceConfig, owner, repoName, issueTitle, issueBody) {
	const url = `${instanceConfig.baseUrl}/api/v1/repos/${owner}/${repoName}/issues`;
	const headers = { Authorization: `token ${instanceConfig.apiToken}` };
	const data = {
		title: issueTitle,
		body: issueBody,
	};
	try {
		await axios.post(url, data, { headers });
	} catch (error) {
		console.error(
			`[${instanceConfig.name}] Fehler beim Erstellen des Issues "${issueTitle}" in ${owner}/${repoName}:`,
			error.response ? error.response.data : error.message,
		);
		throw error;
	}
}

async function listRepositories(instanceConfig) {
	const url = `${instanceConfig.baseUrl}/api/v1/user/repos`;
	const headers = { Authorization: `token ${instanceConfig.apiToken}` };
	try {
		await axios.get(url, { headers });
	} catch (error) {
		console.error(`[${instanceConfig.name}] Fehler beim Auflisten der Repos:`, error.response ? error.response.data : error.message);
		throw error;
	}
}

async function listIssues(instanceConfig, owner, repoName) {
	const url = `${instanceConfig.baseUrl}/api/v1/repos/${owner}/${repoName}/issues`;
	const headers = { Authorization: `token ${instanceConfig.apiToken}` };
	try {
		await axios.get(url, { headers });
	} catch (error) {
		console.error(
			`[${instanceConfig.name}] Fehler beim Auflisten der Issues in ${owner}/${repoName}:`,
			error.response ? error.response.data : error.message,
		);
		throw error;
	}
}

async function getAuthenticatedUser(instanceConfig) {
	const url = `${instanceConfig.baseUrl}/api/v1/user`;
	const headers = { Authorization: `token ${instanceConfig.apiToken}` };
	try {
		const response = await axios.get(url, { headers });
		return response.data.login;
	} catch (error) {
		console.error(`[${instanceConfig.name}] Fehler beim Abrufen des Benutzers:`, error.response ? error.response.data : error.message);
		throw error;
	}
}

async function runGiteaInstanceBenchmark(instanceConfig) {
	console.log(`\n--- Starte Benchmark für: ${instanceConfig.name} ---`);
	const results = {
		repoCreationTime: 0,
		issueCreationTime: 0,
		repoListingTime: 0,
		issueListingTime: 0,
		totalErrors: 0,
	};
	let owner;

	try {
		owner = await getAuthenticatedUser(instanceConfig);
		if (!owner) {
			console.error(`[${instanceConfig.name}] Konnte Benutzer nicht ermitteln. API Token korrekt und aktiv?`);
			results.totalErrors++;
			return results;
		}
		console.log(`[${instanceConfig.name}] Tests werden als Benutzer "${owner}" ausgeführt.`);

		// --- 1. Repositories erstellen (Schreiblast) ---
		const createdRepoNames = [];
		let startTime = performance.now();
		for (let i = 0; i < NUM_REPO_CREATIONS; i++) {
			const repoName = `perf-test-repo-${generateRandomString()}`;
			try {
				await createRepository(instanceConfig, repoName);
				createdRepoNames.push(repoName);
			} catch (e) {
				results.totalErrors++;
			}
			await sleep(50);
		}
		results.repoCreationTime = performance.now() - startTime;
		console.log(`[${instanceConfig.name}] ${NUM_REPO_CREATIONS} Repos erstellt in ${results.repoCreationTime.toFixed(2)} ms`);

		// --- 2. Issues erstellen (Schreiblast) ---
		if (createdRepoNames.length > 0) {
			startTime = performance.now();
			for (const repoName of createdRepoNames) {
				for (let i = 0; i < NUM_ISSUE_CREATIONS_PER_REPO; i++) {
					try {
						await createIssue(
							instanceConfig,
							owner,
							repoName,
							`Test Issue ${i + 1} ${generateRandomString(4)}`,
							"Dies ist ein automatischer Test-Issue.",
						);
					} catch (e) {
						results.totalErrors++;
					}
					await sleep(30);
				}
			}
			results.issueCreationTime = performance.now() - startTime;
			const totalIssuesCreated = createdRepoNames.length * NUM_ISSUE_CREATIONS_PER_REPO;
			console.log(`[${instanceConfig.name}] ${totalIssuesCreated} Issues erstellt in ${results.issueCreationTime.toFixed(2)} ms`);
		} else {
			console.log(`[${instanceConfig.name}] Keine Repos erstellt, überspringe Issue-Erstellung.`);
		}

		// --- 3. Repository-Liste abrufen (Leselast) ---
		startTime = performance.now();
		for (let i = 0; i < NUM_REPO_LISTINGS; i++) {
			try {
				await listRepositories(instanceConfig);
			} catch (e) {
				results.totalErrors++;
			}
			await sleep(20);
		}
		results.repoListingTime = performance.now() - startTime;
		console.log(`[${instanceConfig.name}] ${NUM_REPO_LISTINGS} Mal Repos gelistet in ${results.repoListingTime.toFixed(2)} ms`);

		// --- 4. Issues eines Repos abrufen (Leselast) ---
		if (createdRepoNames.length > 0) {
			const repoToTestIssues = createdRepoNames[0];
			startTime = performance.now();
			for (let i = 0; i < NUM_ISSUE_LISTINGS_PER_REPO; i++) {
				try {
					await listIssues(instanceConfig, owner, repoToTestIssues);
				} catch (e) {
					results.totalErrors++;
				}
				await sleep(20);
			}
			results.issueListingTime = performance.now() - startTime;
			console.log(
				`[${instanceConfig.name}] ${NUM_ISSUE_LISTINGS_PER_REPO} Mal Issues von '${repoToTestIssues}' gelistet in ${results.issueListingTime.toFixed(2)} ms`,
			);
		} else {
			console.log(`[${instanceConfig.name}] Keine Repos für Issue-Listing Test vorhanden.`);
		}

		// --- Aufräumen ---
		console.log(`[${instanceConfig.name}] Starte Aufräumarbeiten...`);
		for (const repoName of createdRepoNames) {
			try {
				const url = `${instanceConfig.baseUrl}/api/v1/repos/${owner}/${repoName}`;
				const headers = { Authorization: `token ${instanceConfig.apiToken}` };
				await axios.delete(url, { headers });
			} catch (error) {
				console.error(
					`[${instanceConfig.name}] Fehler beim Löschen des Repos ${repoName}:`,
					error.response ? error.response.data : error.message,
				);
				results.totalErrors++;
			}
			await sleep(50);
		}
		console.log(`[${instanceConfig.name}] Aufräumarbeiten abgeschlossen.`);
	} catch (mainError) {
		console.error(`[${instanceConfig.name}] Schwerwiegender Fehler im Benchmark-Durchlauf:`, mainError.message);
		results.totalErrors++;
	}
	console.log(`[${instanceConfig.name}] Benchmark abgeschlossen mit ${results.totalErrors} Fehlern.`);
	return results;
}

async function main() {
	console.log("Starte Gitea Performance Benchmark über API...");
	const allResults = {};

	for (const instance of GITEA_INSTANCES) {
		console.log(`\nInitialisiere Test für ${instance.name}. Kurze Pause...`);
		await sleep(5000);

		const instanceResults = await runGiteaInstanceBenchmark(instance);
		allResults[instance.name] = instanceResults;
		await sleep(2000);
	}

	console.log("\n\n--- GITEA API BENCHMARK ERGEBNISSE ---");
	for (const instanceName in allResults) {
		const res = allResults[instanceName];
		console.log(`\nErgebnisse für: ${instanceName}`);
		console.log(`  Repo Erstellung (${NUM_REPO_CREATIONS} Repos):`.padEnd(45) + `${res.repoCreationTime.toFixed(2)} ms`);
		const totalIssues = NUM_REPO_CREATIONS * NUM_ISSUE_CREATIONS_PER_REPO;
		console.log(`  Issue Erstellung (${totalIssues} Issues):`.padEnd(45) + `${res.issueCreationTime.toFixed(2)} ms`);
		console.log(`  Repo Listing (${NUM_REPO_LISTINGS} Abrufe):`.padEnd(45) + `${res.repoListingTime.toFixed(2)} ms`);
		console.log(`  Issue Listing (${NUM_ISSUE_LISTINGS_PER_REPO} Abrufe):`.padEnd(45) + `${res.issueListingTime.toFixed(2)} ms`);
		console.log(`  Gesamte Fehler:`.padEnd(45) + `${res.totalErrors}`);
	}
	console.log("\nBenchmark abgeschlossen.");
}

main().catch((err) => {
	console.error("Unerwarteter Fehler in der Hauptausführung:", err);
});

Ergebnisse des API-Tests

Nachdem ich den Test mehrfach habe laufen lassen, um Ausreißer zu minimieren, ergab sich folgendes Bild (Zeiten in Millisekunden, weniger ist besser):

OperationSQLiteMariaDB (MySQL)PostgreSQL
Repo-Erstellung (150)18.969,62 ms23.659,65 ms20.521,67 ms
Issue-Erstellung (750)68.786,91 ms87.232,65 ms83.554,48 ms
Repo-Liste (150 Abrufe)13.114,50 ms27.110,95 ms22.267,44 ms
Issue-Liste (5 Abrufe)310,68 ms506,04 ms508,11 ms

Analyse der API-Ergebnisse

Das Ergebnis ist verblüffend:

  • SQLite hat in allen Kategorien gewonnen – und das teilweise deutlich! Bei vielen kleinen Schreibvorgängen (Issue-Erstellung) und bei den Leseoperationen war der geringe Overhead der dateibasierten Datenbank ein klarer Vorteil. Da es keinen Netzwerk-Roundtrip zwischen Gitea-Container und Datenbank-Container gibt, fallen Latenzen weg.

Zwischenfazit: Für den typischen Anwendungsfall einer kleinen bis mittelgroßen Gitea-Instanz scheint SQLite eine überraschend performante Wahl zu sein. Die Einfachheit zahlt sich hier in Geschwindigkeit aus.

Benchmark 2: Direkter Datenbank-Showdown – Wer ist roh am schnellsten?

Der erste Test war praxisnah, aber durch die Gitea-Anwendung beeinflusst. Ich wollte wissen, wie sich die Datenbanken im direkten Vergleich schlagen. Dazu habe ich die Gitea-Container gelöscht und nur noch mariaDB in der Version 11 sowie Postgres in der Version 17 laufen lassen, deren Ports ich direkt freigegeben habe.

Auf meinem Test-Server habe ich ein zweites Node.js-Skript geschrieben, das sich direkt mit den Datenbanken verbindet und zwei Tests durchführt:

  1. Schreibtest: 100.000 Zeilen in Batches von 1000 in eine einfache Tabelle einfügen.
  2. Lesetest: 100.000 Zeilen einzeln über ihre ID auslesen.

Eine lokale SQLite-Datenbankdatei auf dem Testserver diente als dritter Vergleichspartner.

const { Client } = require("pg");
const mysql = require("mysql2/promise");
const sqlite3 = require("sqlite3").verbose();
const { performance } = require("perf_hooks");
const path = require("path");

// --- Konfiguration ---
const MARIA_DB_CONFIG = {
	host: "191.199.123.123",
	user: "benchmarkuser",
	password: "einStarkesPasswort",
	database: "test_datenbank",
	port: 3306,
	connectTimeout: 10000,
};

const POSTGRES_CONFIG = {
	host: "138.199.123.123",
	user: "benchmarkuser",
	password: "einStarkesPasswort",
	database: "test_datenbank",
	port: 5432,
	connectionTimeoutMillis: 10000,
};

const SQLITE_DB_PATH = path.join(__dirname, "benchmark_sqlite.db");

const TEST_TABLE_NAME = "benchmark_items";
const NUM_WRITES = 100000;
const NUM_READS = 100000;
const BATCH_SIZE_WRITE = 1000;

// --- Hilfsfunktionen ---
function generateRandomString(length = 10) {
	return Math.random()
		.toString(36)
		.substring(2, 2 + length);
}
async function sleep(ms) {
	return new Promise((resolve) => setTimeout(resolve, ms));
}

// --- MariaDB Testfunktionen ---
async function runMariaDBBenchmark() {
	console.log("\n--- Starte MariaDB Benchmark ---");
	let connection;
	const results = { writeTime: 0, readTime: 0, error: null };

	try {
		console.log(`Verbinde mit MariaDB auf ${MARIA_DB_CONFIG.host}...`);
		connection = await mysql.createConnection(MARIA_DB_CONFIG);
		console.log("MariaDB verbunden.");

		// 1. Tabelle erstellen (wenn nicht vorhanden)
		console.log(`Erstelle Tabelle ${TEST_TABLE_NAME} (falls nicht vorhanden)...`);
		await connection.execute(`DROP TABLE IF EXISTS ${TEST_TABLE_NAME}`);
		await connection.execute(`
            CREATE TABLE ${TEST_TABLE_NAME} (
                id INT AUTO_INCREMENT PRIMARY KEY,
                name VARCHAR(255) NOT NULL,
                description TEXT,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        `);
		console.log(`Tabelle ${TEST_TABLE_NAME} erstellt.`);

		// 2. Schreibtest
		console.log(`Starte Schreibtest (${NUM_WRITES} Datensätze in Batches von ${BATCH_SIZE_WRITE})...`);
		let writeStartTime = performance.now();
		let itemsToWrite = [];
		for (let i = 0; i < NUM_WRITES; i++) {
			itemsToWrite.push([`ItemName ${generateRandomString(8)}`, `Description ${generateRandomString(50)}`]);
			if (itemsToWrite.length === BATCH_SIZE_WRITE || i === NUM_WRITES - 1) {
				const query = `INSERT INTO ${TEST_TABLE_NAME} (name, description) VALUES ?`;
				await connection.query(query, [itemsToWrite]);
				itemsToWrite = [];
			}
		}
		results.writeTime = performance.now() - writeStartTime;
		console.log(`Schreibtest abgeschlossen in ${results.writeTime.toFixed(2)} ms`);

		// Kurze Pause
		await sleep(1000);

		// 3. Lesetest (liest alle IDs und dann zufällige Datensätze)
		console.log(`Starte Lesetest (${NUM_READS} Datensätze)...`);
		const [rows] = await connection.execute(`SELECT id FROM ${TEST_TABLE_NAME}`);
		const ids = rows.map((row) => row.id);

		if (ids.length === 0) {
			console.log("Keine Daten zum Lesen vorhanden.");
			results.readTime = 0;
		} else {
			let readStartTime = performance.now();
			for (let i = 0; i < NUM_READS; i++) {
				const randomId = ids[Math.floor(Math.random() * ids.length)];
				await connection.execute(`SELECT * FROM ${TEST_TABLE_NAME} WHERE id = ?`, [randomId]);
			}
			results.readTime = performance.now() - readStartTime;
			console.log(`Lesetest abgeschlossen in ${results.readTime.toFixed(2)} ms`);
		}
	} catch (err) {
		console.error("MariaDB Benchmark Fehler:", err.message);
		results.error = err.message;
	} finally {
		if (connection) {
			console.log("Schließe MariaDB Verbindung.");
			await connection.end();
		}
	}
	return results;
}

// --- PostgreSQL Testfunktionen ---
async function runPostgresBenchmark() {
	console.log("\n--- Starte PostgreSQL Benchmark ---");
	const client = new Client(POSTGRES_CONFIG);
	const results = { writeTime: 0, readTime: 0, error: null };

	try {
		console.log(`Verbinde mit PostgreSQL auf ${POSTGRES_CONFIG.host}...`);
		await client.connect();
		console.log("PostgreSQL verbunden.");

		// 1. Tabelle erstellen
		console.log(`Erstelle Tabelle ${TEST_TABLE_NAME} (falls nicht vorhanden)...`);
		await client.query(`DROP TABLE IF EXISTS ${TEST_TABLE_NAME}`);
		await client.query(`
            CREATE TABLE ${TEST_TABLE_NAME} (
                id SERIAL PRIMARY KEY,
                name VARCHAR(255) NOT NULL,
                description TEXT,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        `);
		console.log(`Tabelle ${TEST_TABLE_NAME} erstellt.`);

		// 2. Schreibtest
		console.log(`Starte Schreibtest (${NUM_WRITES} Datensätze in Batches von ${BATCH_SIZE_WRITE})...`);
		let writeStartTime = performance.now();
		let itemsBuffer = [];
		let valuePlaceholders = [];
		for (let i = 0; i < NUM_WRITES; i++) {
			itemsBuffer.push(`ItemName ${generateRandomString(8)}`);
			itemsBuffer.push(`Description ${generateRandomString(50)}`);
			valuePlaceholders.push(`($${itemsBuffer.length - 1}, $${itemsBuffer.length})`);

			if (valuePlaceholders.length === BATCH_SIZE_WRITE || i === NUM_WRITES - 1) {
				const queryText = `INSERT INTO ${TEST_TABLE_NAME} (name, description) VALUES ${valuePlaceholders.join(", ")}`;
				await client.query(queryText, itemsBuffer);
				itemsBuffer = [];
				valuePlaceholders = [];
			}
		}
		results.writeTime = performance.now() - writeStartTime;
		console.log(`Schreibtest abgeschlossen in ${results.writeTime.toFixed(2)} ms`);

		// Kurze Pause
		await sleep(1000);

		// 3. Lesetest
		console.log(`Starte Lesetest (${NUM_READS} Datensätze)...`);
		const res = await client.query(`SELECT id FROM ${TEST_TABLE_NAME}`);
		const ids = res.rows.map((row) => row.id);

		if (ids.length === 0) {
			console.log("Keine Daten zum Lesen vorhanden.");
			results.readTime = 0;
		} else {
			let readStartTime = performance.now();
			for (let i = 0; i < NUM_READS; i++) {
				const randomId = ids[Math.floor(Math.random() * ids.length)];
				await client.query(`SELECT * FROM ${TEST_TABLE_NAME} WHERE id = $1`, [randomId]);
			}
			results.readTime = performance.now() - readStartTime;
			console.log(`Lesetest abgeschlossen in ${results.readTime.toFixed(2)} ms`);
		}
	} catch (err) {
		console.error("PostgreSQL Benchmark Fehler:", err.message);
		results.error = err.stack; // stack für mehr Details
	} finally {
		if (client) {
			console.log("Schließe PostgreSQL Verbindung.");
			await client.end();
		}
	}
	return results;
}

// --- SQLite Testfunktionen ---
async function runSQLiteBenchmark() {
	console.log("\n--- Starte SQLite Benchmark ---");
	const results = { writeTime: 0, readTime: 0, error: null };

	const openDb = () =>
		new Promise((resolve, reject) => {
			const db = new sqlite3.Database(SQLITE_DB_PATH, (err) => {
				if (err) reject(err);
				else resolve(db);
			});
		});
	const dbRun = (db, sql, params = []) =>
		new Promise((resolve, reject) => {
			db.run(sql, params, function (err) {
				if (err) reject(err);
				else resolve(this);
			});
		});
	const dbAll = (db, sql, params = []) =>
		new Promise((resolve, reject) => {
			db.all(sql, params, (err, rows) => {
				if (err) reject(err);
				else resolve(rows);
			});
		});
	const dbClose = (db) =>
		new Promise((resolve, reject) => {
			db.close((err) => {
				if (err) reject(err);
				else resolve();
			});
		});

	let db;
	try {
		console.log(`Öffne/Erstelle SQLite Datenbank: ${SQLITE_DB_PATH}`);
		db = await openDb();
		console.log("SQLite DB geöffnet.");

		await dbRun(db, "PRAGMA synchronous = OFF");
		await dbRun(db, "PRAGMA journal_mode = MEMORY;");

		// 1. Tabelle erstellen
		console.log(`Erstelle Tabelle ${TEST_TABLE_NAME} (falls nicht vorhanden)...`);
		await dbRun(db, `DROP TABLE IF EXISTS ${TEST_TABLE_NAME}`);
		await dbRun(
			db,
			`
            CREATE TABLE ${TEST_TABLE_NAME} (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL,
                description TEXT,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        `,
		);
		console.log(`Tabelle ${TEST_TABLE_NAME} erstellt.`);

		// 2. Schreibtest (für SQLite sind einzelne Inserts in einer Transaktion oft am besten)
		console.log(`Starte Schreibtest (${NUM_WRITES} Datensätze)...`);
		let writeStartTime = performance.now();
		await dbRun(db, "BEGIN TRANSACTION");
		const stmt = await new Promise((resolve, reject) => {
			const prepStmt = db.prepare(`INSERT INTO ${TEST_TABLE_NAME} (name, description) VALUES (?, ?)`, (err) => {
				if (err) reject(err);
				else resolve(prepStmt);
			});
		});

		for (let i = 0; i < NUM_WRITES; i++) {
			await new Promise((resolve, reject) => {
				stmt.run([`ItemName ${generateRandomString(8)}`, `Description ${generateRandomString(50)}`], function (err) {
					if (err) reject(err);
					else resolve(this);
				});
			});
		}
		await new Promise((resolve, reject) => {
			stmt.finalize((err) => {
				if (err) reject(err);
				else resolve();
			});
		});
		await dbRun(db, "COMMIT");
		results.writeTime = performance.now() - writeStartTime;
		console.log(`Schreibtest abgeschlossen in ${results.writeTime.toFixed(2)} ms`);

		await sleep(1000);

		// 3. Lesetest
		console.log(`Starte Lesetest (${NUM_READS} Datensätze)...`);
		const rows = await dbAll(db, `SELECT id FROM ${TEST_TABLE_NAME}`);
		const ids = rows.map((row) => row.id);

		if (ids.length === 0) {
			console.log("Keine Daten zum Lesen vorhanden.");
			results.readTime = 0;
		} else {
			let readStartTime = performance.now();
			const readStmt = await new Promise((resolve, reject) => {
				const prep = db.prepare(`SELECT * FROM ${TEST_TABLE_NAME} WHERE id = ?`, (err) => {
					if (err) reject(err);
					else resolve(prep);
				});
			});
			for (let i = 0; i < NUM_READS; i++) {
				const randomId = ids[Math.floor(Math.random() * ids.length)];
				await new Promise((resolve, reject) => {
					readStmt.get([randomId], (err, row) => {
						if (err) reject(err);
						else resolve(row);
					});
				});
			}
			await new Promise((resolve, reject) => {
				readStmt.finalize((err) => {
					if (err) reject(err);
					else resolve();
				});
			});
			results.readTime = performance.now() - readStartTime;
			console.log(`Lesetest abgeschlossen in ${results.readTime.toFixed(2)} ms`);
		}
	} catch (err) {
		console.error("SQLite Benchmark Fehler:", err.message);
		results.error = err.message;
	} finally {
		if (db) {
			console.log("Schließe SQLite DB.");
			try {
				await dbClose(db);
			} catch (closeErr) {
				console.error("Fehler beim Schließen der SQLite DB:", closeErr.message);
			}
		}
	}
	return results;
}

// --- Hauptausführung ---
async function main() {
	console.log("Starte REINEN Datenbank Performance Benchmark (via Docker/Lokal)...");
	console.log(`Parameter: ${NUM_WRITES} Schreibvorgänge, ${NUM_READS} Lesevorgänge`);
	const allResults = {};

	// Test SQLite
	allResults.SQLite = await runSQLiteBenchmark();
	await sleep(2000);

	// Test MariaDB
	allResults.MariaDB = await runMariaDBBenchmark();
	await sleep(2000);

	// Test Postgres
	allResults.PostgreSQL = await runPostgresBenchmark();

	console.log("\n\n--- REINER DATENBANK BENCHMARK ERGEBNISSE ---");

	const printResult = (dbName, res) => {
		console.log(`\n${dbName}:`);
		if (res.error) {
			console.log(`  Fehler: ${res.error}`);
		} else {
			console.log(`  Schreibzeit (${NUM_WRITES} Sätze):`.padEnd(45) + `${res.writeTime.toFixed(2)} ms`);
			console.log(`  Lesezeit (${NUM_READS} Sätze):`.padEnd(45) + `${res.readTime.toFixed(2)} ms`);
		}
	};

	printResult("SQLite (Lokal)", allResults.SQLite);
	printResult("MariaDB (Docker auf VPS)", allResults.MariaDB);
	printResult("PostgreSQL (Docker auf VPS)", allResults.PostgreSQL);

	console.log("\nBenchmark abgeschlossen.");
}

main().catch((err) => {
	console.error("Unerwarteter Fehler in der Hauptausführung:", err);
});

Ergebnisse des direkten Datenbanktests

Die Ergebnisse (wieder in Millisekunden) zeigen ein anderes Bild:

OperationSQLiteMariaDB (MySQL)PostgreSQL
Schreibtest (100k)23.386,672.095,292.801,28
Lesetest (100k)32.989,38106.746,85105.027,99

Analyse der direkten Testergebnisse

  • Schreiben: Hier zeigen die “großen” Datenbanken ihre Stärke. MariaDB und PostgreSQL sind beim massenhaften Einfügen von Daten um den Faktor 10 schneller als SQLite. Effizientes Batching und parallele Verarbeitung zahlen sich hier voll aus.
  • Lesen: Beim gezielten Lesen einzelner Datensätze ist SQLite wieder unschlagbar schnell. Der fehlende Netzwerk-Overhead und die wahrscheinlich sehr effiziente Indexnutzung auf einer lokalen Datei sind hier die entscheidenden Vorteile.

Fazit: Und der Gewinner ist… es kommt darauf an!

Dieser Test hat eindrucksvoll gezeigt, dass es nicht die eine beste Datenbank gibt. Die Wahl hängt stark vom Anwendungsfall ab.

  1. SQLite: Die Überraschung des Tests. Für persönliche Gitea-Instanzen oder kleine Teams ist SQLite eine absolut valide und extrem performante Option. Die einfache Einrichtung (kein separater Container), der geringe Ressourcenverbrauch und die exzellente Lese-Performance machen es zum heimlichen Champion für kleine Setups. Der Nachteil ist die geringere Skalierbarkeit und das Fehlen fortgeschrittener Datenbank-Features.

  2. PostgreSQL: Die professionelle Wahl. Wenn du planst, dass deine Gitea-Instanz wächst, viele Benutzer haben wird oder du auf Features wie komplexe Abfragen und hohe Datenintegrität angewiesen bist, ist PostgreSQL die beste Wahl. Es ist robust, extrem skalierbar und hat sich bei der Erstellung von Repositories als am schnellsten erwiesen.

  3. MariaDB (MySQL): Ein solider Allrounder. Auch wenn es in meinen spezifischen Tests nicht die Spitzenposition erreicht hat, ist MariaDB eine sehr fähige und weit verbreitete Datenbank. Wenn du bereits Erfahrung mit MySQL hast oder andere Anwendungen in deinem Stack es nutzen, ist es eine absolut sichere und gute Wahl.

Die Wahl der richtigen Datenbank sollte sich jedoch nicht nur an reinen Performance-Zahlen orientieren. Andere, oft wichtigere Faktoren sind:

  • Skalierbarkeit: Wie viele Benutzer und wie viel Datenwachstum erwartest du? Hier sind PostgreSQL und MariaDB klar im Vorteil.
  • Datenintegrität: Benötigst du fortgeschrittene Transaktionskontrollen und Constraints, die über das hinausgehen, was SQLite bietet?
  • Feature-Set: Sind spezielle Funktionen wie JSONB-Support (PostgreSQL) oder bestimmte Replikationsmechanismen (MariaDB) wichtig?
  • Betriebsaufwand: Bist du bereit, einen separaten Datenbank-Container zu verwalten und zu sichern, oder bevorzugst du die Einfachheit einer dateibasierten Lösung?

Meine persönliche Empfehlung?

  • Für einen schnellen Start oder ein kleines, privates Projekt: Gib SQLite eine Chance. Du wirst von der Performance beeindruckt sein.
  • Für ein wachsendes Team oder wenn du auf Nummer sicher gehen willst: Wähle PostgreSQL. Es bietet die beste Grundlage für zukünftiges Wachstum.

Ich hoffe, dieser detaillierte Vergleich hilft dir bei deiner Entscheidung. Welche Datenbank nutzt du für deine selbstgehosteten Anwendungen und warum? Schreib es gerne in die Kommentare!

FAQs

Ist SQLite wirklich 'produktionsreif'?

Ja, für viele Anwendungsfälle absolut. Für persönliche Projekte, kleine Teams oder Server mit begrenzten Ressourcen ist es oft die beste Wahl, da es einfach zu verwalten ist und, wie der Test zeigt, sehr performant sein kann. Bei sehr vielen gleichzeitigen Schreibzugriffen oder sehr großen Instanzen könnten PostgreSQL oder MariaDB jedoch Vorteile haben.

Diesen Beitrag teilen:

Diese Website verwendet Cookies. Diese sind notwendig, um die Funktionalität der Website zu gewährleisten. Weitere Informationen finden Sie in der Datenschutzerklärung