Skip to content

High peformance in-memory database with flexible indexing to speed up query speed.

License

Notifications You must be signed in to change notification settings

hugjobk/go-imdb

Repository files navigation

High peformance in-memory database with flexible indexing to speed up query speed.

Usage

A simple example of using imdb::Database to store and query data.

package imdb_test

import (
	"fmt"
	"strings"
	"sync/atomic"
	"testing"

	"github.com/hugjobk/imdb"
)

// Define a record struct.
type record struct {
	Id    int
	Email string
	Name  string
	Age   int
}

// Utility function to print query results.
func printResults(recs []interface{}) string {
	var arr []string
	for i := range recs {
		arr = append(arr, fmt.Sprintf("%+v", recs[i].(record)))
	}
	return fmt.Sprintf("[%s]", strings.Join(arr, ", "))
}

// Simple test for creating, indexing, insertion and query database.
func TestDatabase(t *testing.T) {
	db := imdb.NewDatabase() // Create new database.
	db.UniqueIndex("Id")     // Create a unique index on field `Id`.
	db.UniqueIndex("Email")  // Create a unique index on field `Email`.
	db.Index("Age")          // Create a non-unique index on field `Age`.
	db.Index("Age", "Name")  // Create a non-unique index on fields `Age` and `Name`.

	// Note: unique index is normally faster than non-unique index.
	// If a query mathes both unique and non-unique index, it will choose unique index to query.

	// Add first recrod.
	if err := db.Add(record{1, "email1", "name1", 20}); err != nil {
		t.Log(err)
	}
	// Add second record.
	if err := db.Add(record{2, "email2", "name2", 20}); err != nil {
		t.Log(err)
	}
	// Add third record.
	if err := db.Add(record{3, "email3", "name3", 21}); err != nil {
		t.Log(err)
	}
	// Add forth record.
	if err := db.Add(record{4, "email4", "name4", 21}); err != nil {
		t.Log(err)
	}
	// Add fifth record.
	// This should return an error since a record with `Id` = 1 already exists (violate unique index on `Id`).
	if err := db.Add(record{1, "email5", "name5", 20}); err != nil {
		t.Log(err)
	}

	// Note: you can query by any field even if it is not indexed.
	// However it can be slow because it has to scan the database to find matched records.
	// You can use PrepareFilter to reuse a query multiple times for different query parameters.

	// Build a query where `Id` = 3.
	q1 := db.Query().Filter("Id", 3).Build()
	// Build a query where `Email` = "email2".
	q2 := db.Query().Filter("Email", "email2").Build()
	// Build a query where `Age` = 21.
	q3 := db.Query().Filter("Age", 21).Build()
	// Build a query where `Name` = "name4" AND `Age` = 21.
	q4 := db.Query().Filter("Name", "name4").Filter("Age", 21).Build()

	// Print the queries and their results.
	t.Logf("%s -> %s", q1, printResults(q1.Run()))
	t.Logf("%s -> %s", q2, printResults(q2.Run()))
	t.Logf("%s -> %s", q3, printResults(q3.Run()))
	t.Logf("%s -> %s", q4, printResults(q4.Run()))
}

var db *imdb.Database

var DatabaseSize uint32

func init() {
	// Create a database and indexes to speed up query speed.
	db = imdb.NewDatabase()
	db.UniqueIndex("Id")
	db.UniqueIndex("Email")
	db.Index("Name", "Age")
}

func BenchmarkAdd(b *testing.B) {
	b.RunParallel(func(p *testing.PB) {
		for p.Next() {
			i := atomic.AddUint32(&DatabaseSize, 1)
			id := int(i)
			email := fmt.Sprintf("email%d", i)
			name := fmt.Sprintf("name%d", i%10000)
			age := int(i % 100)
			db.Add(record{id, email, name, age})
		}
	})
}

func BenchmarkQueryById(b *testing.B) {
	var c uint32
	b.RunParallel(func(p *testing.PB) {
		var id int
		q := db.Query().PrepareFilter("Id", &id).Build()
		for p.Next() {
			i := atomic.AddUint32(&c, 1)
			id = int(i % uint32(DatabaseSize))
			q.Run()
		}
	})
}

func BenchmarkQueryByEmail(b *testing.B) {
	var c uint32
	b.RunParallel(func(p *testing.PB) {
		var email string
		q := db.Query().PrepareFilter("Email", &email).Build()
		for p.Next() {
			i := atomic.AddUint32(&c, 1)
			email = fmt.Sprintf("email%d", i%DatabaseSize)
			q.Run()
		}
	})
}

func BenchmarkQueryByNameAndAge(b *testing.B) {
	var c uint32
	b.RunParallel(func(p *testing.PB) {
		var name string
		var age int
		q := db.Query().PrepareFilter("Name", &name).PrepareFilter("Age", &age).Build()
		for p.Next() {
			i := atomic.AddUint32(&c, 1)
			name = fmt.Sprintf("name%d", i%10000)
			age = int(i % 100)
			q.Run()
		}
	})
}

Run test example:

$ go test -v github.com/hugjobk/imdb

Output:

=== RUN   TestDatabase
    database_test.go:59: uniqueIndex{Id} violated
    database_test.go:76: (Id = 3) -> [{Id:3 Email:email3 Name:name3 Age:21}]
    database_test.go:77: (Email = email2) -> [{Id:2 Email:email2 Name:name2 Age:20}]
    database_test.go:78: (Age = 21) -> [{Id:3 Email:email3 Name:name3 Age:21}, {Id:4 Email:email4 Name:name4 Age:21}]
    database_test.go:79: (Name = name4 & Age = 21) -> [{Id:4 Email:email4 Name:name4 Age:21}]
--- PASS: TestDatabase (0.00s)
PASS
ok      github.com/hugjobk/imdb 0.030s

Run benchmark:

$ go test -benchmem -bench . github.com/hugjobk/imdb

Output:

goos: windows
goarch: amd64
pkg: github.com/hugjobk/imdb
cpu: Intel(R) Core(TM) i5-6500 CPU @ 3.20GHz
BenchmarkAdd-4                    142882              8530 ns/op             917 B/op         36 allocs/op
BenchmarkQueryById-4             4540394               247.2 ns/op            90 B/op          6 allocs/op
BenchmarkQueryByEmail-4          4620968               280.3 ns/op           122 B/op          7 allocs/op
BenchmarkQueryByNameAndAge-4     1831894               648.2 ns/op           420 B/op         11 allocs/op
PASS
ok      github.com/hugjobk/imdb 6.803s

About

High peformance in-memory database with flexible indexing to speed up query speed.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages