Radek Chalupa   konzultace a školení programování, vývoj software na zakázku

Databáze SQLite v C/C++ - jednoduché testy výkonu

12.11.2019

Databáze SQLite je známa svoji rychlostí a výkonem ve své kategorii. Ostatně je kompletně napsána v jazyce C, takže její rychlost a výkon plně využijeme především v nativních aplikacích napsaných v C popř. C++. Samozřejmě předpokladem je správně a optimálně napsaný zdrojový kód a nastavení optimalizace překladače. Pro testování vytvoříme databází obsahující v položce kromě unikátního klíče (autoinkrementační) opět 2 sloupce: číslo a text. Do této souborová databáze zkusíme vygenerovat 100 000 000 (slovy 100 milionů) řádků. Nejprve kód funkce generující tyto položky

void naplnit_db_cislo_text(size_t pocet = 100000000)
{
	assert(_sqlite3);
	srand(time(nullptr));
	char text[255];
	int cislo;
	int ires;
	ires = sqlite3_exec(_sqlite3, "BEGIN TRANSACTION", nullptr, nullptr, nullptr);
	if (SQLITE_OK != ires)
		throw std::runtime_error(sqlite3_errstr(ires));
	for (size_t i = 0; i < pocet; i++)
	{
		cislo = rand();
		sprintf(text, "INSERT INTO polozky (cislo, nazev) VALUES (%d, '%d')", cislo, cislo);
		ires = sqlite3_exec(_sqlite3, text, nullptr, nullptr, nullptr);
		if (ires != SQLITE_OK)
			throw std::runtime_error(sqlite3_errstr(ires));
	}
	ires = sqlite3_exec(_sqlite3, "END TRANSACTION", nullptr, nullptr, nullptr);
	if (SQLITE_OK != ires)
		throw std::runtime_error(sqlite3_errstr(ires));
}

Pro optimální rychlost je v tomto případě zcela bezpodmínečné celý cyklus dát do tzv. transakce, tj. na začátku zavolat (pomocí již známe funkce sqlite3_exec) příkaz "BEGIN TRANSACTION" a po ukončení "END TRANSACTION". Bez této transakce se ani neodvažuji domýšlet jak dlouho by celý kód trval. Uvedený kód jsem testoval na PC s procesorem i5 3.2 GHz, 8G RAM s databází na starším SSD disku a trval cca 307 sekund.

Následně jsem vytvořil funkce pro projití všech záznamů a vypočítání součtu zbytků po dělení 10 každého čísla v záznamu, a to oběma dříve zmíněnými způsoby, tj. callback funkcí a "prepared statements".

int callback_cisla(void* p, int count, char** data, char** col_name)
{
	_soucet += atol(*data)%10;
	return 0;
}

void projit_cisla_callback()
{
	assert(_sqlite3);
	_soucet = 0;
	int ires = sqlite3_exec(_sqlite3, "SELECT cislo FROM polozky ORDER BY nazev",
		callback_cisla, nullptr, nullptr);
	if (SQLITE_OK != ires)
		throw std::runtime_error(sqlite3_errstr(ires));
}

void projit_cisla_stmt()
{
	assert(_sqlite3);
	sqlite3_stmt* stmt;
	char sz_sql[255];
	strcpy(sz_sql, "SELECT cislo FROM polozky");
	int ires = sqlite3_prepare_v2(_sqlite3, sz_sql,
		static_cast(strlen(sz_sql)), &stmt, nullptr);
	if (ires != SQLITE_OK)
		throw std::runtime_error(sqlite3_errstr(ires));
	int istep;
	_soucet = 0;
	istep = sqlite3_step(stmt);
	while (SQLITE_ROW == istep)
	{
		_soucet += sqlite3_column_int64(stmt, 0)%10;
		istep = sqlite3_step(stmt);
	}
	ires = sqlite3_reset(stmt);
	if (ires != SQLITE_OK)
		throw std::runtime_error(sqlite3_errstr(ires));
	ires = sqlite3_finalize(stmt);
	if (ires != SQLITE_OK)
		throw std::runtime_error(sqlite3_errstr(ires));
}

Procházení pomocí "prepared statements" trvalo 9.4 sekundy a přes callback funkci 24.7 sekund. Těch necelých 10 sekund je při 100 milionu záznamů a provedení 2 operací (modulo a součet) na každé položce jistě důkazem o výkonu SQLite. Důvodem 2.5 krát delšího času při použití callback funkce je volání funkce atol na každou položku. Parsování textu plus provádění kontrol na numerické znaky pochopitelně vyžaduje mnohem více procesorového času a protože v reálu jsou čísla uložena "číselně", tak při volání funkce sqlite3_column_int64 tato zátěž na výpočet odpadá.

Proto jsem dále zkusil opět oběma způsoby načítat nyní textovou položku a spočítat počet záznamů u kterých je (v textu) na prvním místě číslice 2.

int callback_texty(void* p, int count, char** data, char** col_name)
{
	if ('2' == **data)
		_pocet++;
	return 0;
}

void projit_texty_callback()
{
	assert(_sqlite3);
	_pocet = 0;
	int ires = sqlite3_exec(_sqlite3, "SELECT nazev FROM polozky",
		callback_texty, nullptr, nullptr);
	if (SQLITE_OK != ires)
		throw std::runtime_error(sqlite3_errstr(ires));
}

void projit_texty_stmt()
{
	assert(_sqlite3);
	sqlite3_stmt* stmt;
	char sz_sql[255];
	strcpy(sz_sql, "SELECT nazev FROM polozky");
	int ires = sqlite3_prepare_v2(_sqlite3, sz_sql,
		static_cast(strlen(sz_sql)), &stmt, nullptr);
	if (ires != SQLITE_OK)
		throw std::runtime_error(sqlite3_errstr(ires));
	int istep;
	_pocet = 0;
	istep = sqlite3_step(stmt);
	while (SQLITE_ROW == istep)
	{
		if ('2' == *((char*)sqlite3_column_text(stmt, 0)))
			_pocet++;
		istep = sqlite3_step(stmt);
	}
	ires = sqlite3_reset(stmt);
	if (ires != SQLITE_OK)
		throw std::runtime_error(sqlite3_errstr(ires));
	ires = sqlite3_finalize(stmt);
	if (ires != SQLITE_OK)
		throw std::runtime_error(sqlite3_errstr(ires));
}

V tomto případě byly výsledky u obou způsobů prakticky shodné, procházení přes callback trvalo 9.89 sekund a přes stmt 9.91 sekund, takže v rámci "chyby měření" totožné.

Závěrem může být konstatovaní že databáze SQLite je v desktopové aplikaci velmi rychlá, výkonná a bez velkých požadavků na systémové zdroje, samozřejmě za předpokladu správně napsaného kódu.