Skip to main content
MastiDB Wink Mascot Logo
MastiDB
OPEN SOURCE

A serious OLAP database engine.
Written in Python.

MastiDB is a minimal, on-disk columnar database engine. It implements the same core ideas that make mature analytical engines fast, at a size you can read in an afternoon.

View on GitHub
Columnar storage
Bitmap indexes
Fast aggregations
Pure Python
Same data. More Masti! ✨
MastiDB Mascot
# 1. Install (MyPyC-compiled hot loops)
$ pip install mastidb
# 2. Ingest Wikipedia sample (~24k edits)
$ mastidb demo wikipedia
# 3. Fast columnar query execution
$ mastidb query -d /tmp/wikipedia \
"SELECT cityName, COUNT(id)
WHERE countryName = 'India'
GROUP BY cityName"
ResultSet · 3 rows · 12ms79 matched
cityName │ COUNT(id)
────────────┼──────────
Delhi │ 34
Bengaluru │ 27
Mumbai │ 18
SQL query
→
Dictionary lookup
→
Bitmap index
→
Aggregate result
Only the columns named in the query are ever read.
HOW IT ALL STARTED

I thought I understood databases. There was one way to find out.

1

The confidence

After years of operating, tuning, and optimising analytical engines, I thought I understood the machinery pretty well.

2

The itch

But I still wanted to follow one query all the way down: which bytes moved, which values were decoded, and where the time actually went.

3

The questionable decision

So I built a columnar database in Python.

It seemed reasonable at the time.

QUERY EXECUTION

Follow a query through the engine

Step through what MastiDB does with one analytical query, from SQL text to result. Bitmaps and integer dictionaries let it skip both the row scan and the string building.

1 / 6
$ SQLSELECT cityName, COUNT(id) WHERE countryName = 'India' GROUP BY cityName
Wikipedia Edits (24,433 rows)
1
Stage 1 of 6
Parse Query
AST Analysis
Engine State
Memory Resident
Query Plan & Column Dependency Tree
Filter Predicate
countryName == 'India'
Target column: countryName.mastidb
Grouping Key
GROUP BY cityName
Target column: cityName.mastidb
Aggregation Function
p0 = COUNT(id)
Row counter (no value decode)
Unreferenced Columns
added, deleted, user, flags
0 bytes read from disk

How Stage 1 Works

The SQL string is parsed into an abstract syntax tree. ParsedQuery computes the dependent columns needed: countryName and cityName. Every other column on disk is left untouched.

Why this is fast: No unnecessary columns are read. The engine knows before touching disk exactly which two column files will participate.
ON-DISK FILE FORMAT

Look inside a segment file (.mastidb)

Every column is stored on disk as two files: a 40-byte binary header and a memory-mapped payload. Hover any region below to trace a value from the logical table down to the bytes that hold it.

  • A 40-byte header (.meta) defines ten 32-bit byte boundaries, and an unpadded payload file (.mastidb) stores the data.
  • Inside that payload, dictionary offsets, sorted UTF-8 values, encoded row IDs, and Roaring Bitmaps sit back-to-back, with no padding between them.
  • Because every row ID is exactly 4 bytes, row i is found by arithmetic (offset + i × 4) instead of by walking an index or following pointers.

Logical Table Editor

Editable
Row
countryName (Editable)
Del
0
1
2
3
4
The table is split into columns and encoded. Hover any byte region on the right to trace it back to the rows it came from.

Memory-Mapped Address Space (.mastidb file)

Continuous binary layout
1. Dictionary Offsets (int32 array)
0
9
16
21
Maps each string ID to its byte start offset in Section 2.
2. Sorted Dictionary (UTF-8 Bytes, no separators)
A
r
g
e
n
t
i
n
a
G
e
r
m
a
n
y
I
n
d
i
a
Values stored back-to-back in sorted order to permit binary search.
3. Encoded Row List (Fixed-width int32 IDs)
R0
2
R1
1
R2
2
R3
0
R4
1
Fixed 4 bytes per row. The address of row i is offset_list + (i * 4).
4. Roaring Bitmaps (Serialized bytes per distinct value)
ID 0(Argentina)
0
0
0
1
0
ID 1(Germany)
0
1
0
0
1
ID 2(India)
1
0
1
0
0
Each distinct value owns a bitmap indicating which rows contain that value.
DESIGN TRADE-OFFS

Core engine techniques

Every design choice here trades speed, space, and complexity against each other. For each one: the problem it solves, what MastiDB does about it, and what that costs.

Columnar segments and fixed addressing

The problem

Row storage reads every column off disk, even the ones the query never asks for.

What MastiDB does

Each column lives in its own immutable file, and every row ID is a fixed 4-byte integer, so row i is found by arithmetic (offset + i * 4) rather than by walking an index.

What it costs

Costs 4 bytes per row even for columns with only a handful of distinct values, and it is a poor fit for single-row OLTP work.

Sorted dictionary encoding

The problem

Storing the same strings again on every row wastes space, and comparing them in Python is slow.

What MastiDB does

Each distinct string is stored once in a sorted dictionary. Queries find a value by binary search in O(log N), and ORDER BY on a string column sorts integers instead of strings.

What it costs

Columns with many distinct values make the dictionary large, and numbers are still stored as strings today, so SUM and AVG parse a float per row.

Roaring Bitmap indexing

The problem

Checking a WHERE clause row by row means reading every row in the table.

What MastiDB does

Ingestion builds a compressed Roaring Bitmap for every distinct value. An equality filter is then one bitmap lookup, and AND and OR are bitwise intersection and union.

What it costs

The bitmaps add roughly 15 to 40% to segment size. Filtering is equality-only today; ranges and inequalities are not indexed yet.

Late materialization

The problem

Turning IDs back into strings for rows that never reach the output is work thrown away.

What MastiDB does

Columns stay as integer IDs through filtering, grouping, and sorting. Only the rows that survive LIMIT, or the distinct output groups, are decoded back into strings.

What it costs

A non-aggregate query with a large LIMIT ends up doing many random seeks into the dictionary files to materialize its columns.

Mergeable partial aggregation

The problem

Every segment has its own dictionary, so partial results from two segments cannot simply be concatenated.

What MastiDB does

Each segment returns an AggregatePartial whose merge() is associative and commutative. AVG, for instance, carries (sum, count) until the very end, so partials from any number of segments combine safely.

What it costs

Group keys have to be decoded to strings before they cross a segment boundary, since two segments map the same string to different IDs.

MyPyC ahead-of-time compilation

The problem

Interpreter overhead and dynamic dispatch dominate the cost of a tight inner loop in Python.

What MastiDB does

The hot inner loops are type-annotated and compiled to native C extensions with MyPyC, which cut runtime by about 60% without changing a single algorithm.

What it costs

Needs a C compiler at install time (it falls back to pure Python without one) and holds the compiled modules to strict static types.

PROJECT STATUS

Where it stands today

MastiDB is an experimental project built for learning. It is not meant for production, and it is not trying to compete with mature columnar systems.

● CURRENT CAPABILITIES

What works today

  • ✓ Supported SQL: SELECT, WHERE (equality, AND, OR), GROUP BY, ORDER BY, LIMIT, and aggregations (COUNT, SUM, AVG).
  • ✓ Two-pass execution: string decoding waits until a bounded min-heap has settled which top-K row IDs actually survive.
  • ✓ Independent segments: each segment answers a query on its own and returns a partial result that merges with the others.
Measured gains (1.3M NYPL rows)

Measured on my laptop with a warm cache. MyPyC compilation cut runtime by about 60%, taking COUNT(id) from 8.77s to 3.41s. The value matrix then halved multi-aggregate queries by fetching each column once per chunk, and batched integer reads brought single-segment scans to ~1.45s and equality queries to ~0.15s.

! CURRENT LIMITATIONS

Known rough edges

  • ✕ Everything is a string: every column uses DIMENSION encoding today, so SUM and AVG parse a string on every row. Real numeric types are on the roadmap.
  • ✕ WHERE is equality-only: Bitmap lookups resolve exact equality only (col = val). Ranges, inequalities, and IN lists are not yet supported.
  • ✕ Sequential segment scan: Segments are queried in a sequential Python loop, not fanned out across threads or worker processes.
  • ✕ Cross-segment COUNT(DISTINCT): Partial distinct states are merged on raw per-segment dictionary IDs instead of decoded values, which is a known correctness bug.
  • ✕ Batch-only ingest: Ingestion rewrites the whole target directory rather than appending a segment. Joins and subqueries are out of scope.

Experimental Playground

Expect rough edges, missing features, and an API that still moves. Ideas, bug reports, and patches are all welcome.

MastiDB Construction Mascot - Work in Progress