Database / LadybugDB Interview questions
Last updated
1. What is LadybugDB?
LadybugDB (often just Ladybug) is an embedded property-graph database built for query speed and scalability. It runs inside your application process, so there is no server to install or operate, and you query it with Cypher.
It targets join-heavy analytical workloads on large graphs. Under the hood it uses columnar disk-based storage, CSR adjacency lists, vectorized and factorized execution, multi-core parallelism and serializable ACID transactions. It is MIT licensed.
You can reach it from Python, Node.js, Java, Rust, Go, Swift, C++, C and a CLI. It also interoperates with Parquet, Arrow and DuckDB data, which helps when a graph feeds machine learning or analytics pipelines.
Take quiz
As a standalone server your app connects to over the network
As a managed cloud-only service
As a browser plugin only
In-process, as an embedded library
Join-heavy analytical queries on large graphs
Simple key-value caching only
Time-series downsampling only
Full-page HTML rendering
2. What is the relationship between LadybugDB and Kuzu?
LadybugDB is the project formerly known as Kuzu. The official docs state this directly, and package listings describe Ladybug as a fork made after Kuzu's acquisition, keeping the same embedded, Cypher-based design.
In practice the concepts carry over (structured property graph, COPY FROM, node and rel tables), but the names changed. If you follow older Kuzu tutorials, expect to swap identifiers:
| Item | In Ladybug |
| Python import | import ladybug as lb |
| CLI shell | lbug |
| Node.js package | @ladybugdb/core |
| Rust crate | lbug |
| Database file extension | .lbdb |
| Extension cache | ~/.lbug/extensions |
Take quiz
Memgraph
Kuzu
TigerGraph Lite
Neo4j Embedded
import kuzu as lb
import ladybugdb_server as lb
import ladybug as lb
from neo4j import GraphDatabase
3. What is an embedded graph database?
An embedded graph database is a library that runs inside your application's process. Your code calls it directly; there is no separate daemon, port or login. LadybugDB works this way, which is why a Database object in your program is the database engine.
That Database object bundles the buffer manager, storage manager and transaction manager. Because of that, concurrency rules differ from a server product, which matters when several processes want the same file.
| Embedded (Ladybug) | Client-server |
| Runs in your process | Runs as a separate service |
| No network hop for queries | Every query crosses the network |
| You manage connection lifecycle and concurrency | Server handles pooling and sessions |
Take quiz
Only a REST router
Only a query-result cache for the browser
A cluster coordinator and shard map
Buffer manager, storage manager and transaction manager
No network round trip for each query
Automatic multi-node sharding
Built-in user login screens
Queries are always served by a remote replica
4. What data model does LadybugDB use?
LadybugDB uses a structured property graph model. You still think in nodes, relationships and properties, but you must declare a schema first: node tables, relationship tables, and strongly typed properties.
Node tables need a primary key. Relationship tables connect two node tables and carry optional properties. Each table has exactly one label, and data types follow the Postgres typing system, so a LIST holds elements of one type only.
CREATE NODE TABLE User(name STRING PRIMARY KEY, age INT64); CREATE REL TABLE Follows(FROM User TO User, since DATE);
The practical upshot is that you pick the schema up front, and in return the engine gets typed columns it can scan in vectorized batches. If you truly need schemaless labels, look at open-type graphs created with CREATE GRAPH ... ANY.
Take quiz
Nothing, labels are created implicitly in the default mode
Define node and relationship tables in a schema
Install a separate schema server
Build a secondary index on every property
Up to three
Unlimited
Exactly one
None, labels do not exist
5. What query language does LadybugDB use?
LadybugDB uses Cypher and follows openCypher syntax and semantics wherever it can. If you know Neo4j-style MATCH ... WHERE ... RETURN, most queries read the same.
It adds a few of its own constructs: LOAD FROM for scanning files, SHORTEST and WSHORTEST inside variable-length patterns, a HINT clause for join order, and CALL functions for administration and extensions.
MATCH (a:User)-[:Follows]->(b:User) WHERE a.name = 'Adam' RETURN b.name;
A few practical notes: statements in the CLI must end with a semicolon, configuration uses standalone CALL statements such as CALL THREADS=5 that cannot be combined with RETURN, and parameters use the $name form.
Take quiz
READ CSV
IMPORT FROM
SCAN CSV
LOAD FROM
openCypher
SPARQL
Gremlin traversal steps
MongoDB aggregation pipelines
6. What license does LadybugDB use?
LadybugDB is released under the MIT license, a permissive license. You can embed it in commercial and proprietary applications without opening your own source code.
The core is written in C++ and ships with bindings for several languages, so the license applies the same way whether you call it from Python, Rust, Go or Java.
The permissive license is why Ladybug can ship inside desktop apps, notebooks and edge devices. Releases publish precompiled binaries for the CLI and the C and C++ libraries, and extensions are downloaded separately from the official repository when you run INSTALL.
Take quiz
GPL v3 only
MIT
A commercial-only enterprise license
Server Side Public License
Only non-commercial hobby use
Use only after publishing your app's source
Embedding the database in proprietary products
Use only inside academic labs
7. What files does LadybugDB create on disk?
Since version 0.11.0 LadybugDB stores a database in a single file, for example example.lbdb. That file is the only one you need to open the database; the others are temporary helpers created next to it.
| File | Purpose |
example.lbdb |
Persistent data as of the last checkpoint |
example.lbdb.wal |
Write-ahead log of updates since the last checkpoint |
example.lbdb.shadow |
Exists only while a checkpoint runs |
example.lbdb.tmp |
Spill file under memory pressure |
Before 0.11.0 the path was a directory holding many files.
Because the WAL holds committed changes that have not been merged yet, run CHECKPOINT before copying the .lbdb file on its own, otherwise the copy reflects only the last checkpoint.
Take quiz
The .shadow file
The .tmp file
The history.txt file
The .wal file
When intermediate data must spill because memory is tight
Every time a database opens
Only when you run CHECKPOINT
Only in in-memory mode
8. How do you create an on-disk database in Python?
Pass a file path to Database, then create a Connection from it. If the file does not exist, Ladybug creates it in read-write mode.
import ladybug as lb db = lb.Database("example.lbdb") conn = lb.Connection(db) conn.execute("CREATE NODE TABLE User(name STRING PRIMARY KEY, age INT64)") conn.execute("CREATE (:User {name: 'Alice', age: 30})") print(list(conn.execute("MATCH (u:User) RETURN u.name, u.age")))
The package is published on PyPI (the project page is named real-ladybug); confirm the current name in the install docs. Results can also be pulled as pandas, Polars or Arrow with get_as_df(), get_as_pl() and get_as_arrow().
Keep one Database object per file in a process and create as many Connection objects from it as you need. For readers, open with lb.Database("example.lbdb", read_only=True).
Take quiz
The string :memory:
A file path such as example.lbdb
A server URL
Nothing, persistence is always on
to_pandas_server()
fetch_frame_remote()
get_as_df()
dump_csv()
9. What is an in-memory database in LadybugDB?
If you create a database with an empty path ("") or :memory:, Ladybug runs in in-memory mode. Nothing is written to the WAL or to disk, and everything disappears when the process ends.
It suits quick analysis of small graphs. A few restrictions apply: it cannot be opened READ_ONLY, it cannot be attached, and httpfs remote file caching is not supported for it.
db = lb.Database(":memory:") conn = lb.Connection(db)
Without a WAL there is no durability, so use in-memory mode for tests, notebooks and throwaway analysis, and switch to an on-disk path once the data must survive a restart or no longer fits in memory.
Take quiz
It is saved to a .lbdb file automatically
It is replayed from the WAL
It is uploaded to cloud storage
It is lost
READ_ONLY
READ_WRITE
Creating node tables
Running MATCH queries
10. How do you create a node table in LadybugDB?
Use CREATE NODE TABLE with typed columns and a primary key. The key can be declared inline or as a trailing clause, and columns can carry defaults, including function calls.
CREATE NODE TABLE User( name STRING PRIMARY KEY, age INT64 DEFAULT 0, reg_date DATE, created TIMESTAMP DEFAULT current_timestamp() );
Add IF NOT EXISTS to make the statement safe to re-run. The table name becomes the label you use in MATCH (u:User).
Type names such as STRING, INT64, DATE and TIMESTAMP follow Postgres-style typing. You can also create and fill a node table in one step with CREATE NODE TABLE ... AS LOAD FROM, and change it later with ALTER TABLE or remove it with DROP TABLE.
Take quiz
A foreign key to a rel table
A primary key column
A vector index
An FTS index
Drops and recreates the table
Appends new columns
Does nothing if the table already exists
Raises an error on the second run
11. What is a relationship table in LadybugDB?
A relationship table defines a typed edge between node tables using FROM and TO. Edges are directed, can carry properties, and can span several node-table pairs in one table.
CREATE REL TABLE Follows(FROM User TO User, since DATE); CREATE REL TABLE Knows(FROM User TO User, FROM User TO City);
There is no comma between FROM and TO, but pairs are separated by commas. You cannot declare a primary key on a rel table; each edge gets an internal ID, comparable with ID(r1) != ID(r2).
Create the node tables first, because a rel table names them in FROM and TO. When you later load a rel file with COPY, both endpoint keys in every row must already exist as nodes.
Take quiz
FROM User, TO User, FROM User, TO City
FROM (User, User) TO (User, City)
FROM User TO User AND City
FROM User TO User, FROM User TO City
No, each edge gets an internal ID
Yes, exactly one
Yes, but only STRING
Only in in-memory mode
12. Why does LadybugDB use the word table instead of label?
Because underneath it is a relational engine that stores sets of tuples. A node table is a table of entities; a relationship table is a table of connections. Calling them tables reflects that columns are typed properties and that rels join nodes.
The naming also signals a real constraint: every node or relationship has only one label, the name of its table. A rel table's identity is derived from the primary keys of its FROM and TO nodes, much like a foreign-key join table.
The same relational heritage is why Ladybug can scan and attach relational sources so naturally.
Take quiz
It forbids graph queries
It stores and processes sets of tuples like a relational system
It requires SQL instead of Cypher
It stores everything as JSON documents
Through a hand-written edge key
Through the file offset only
Through the primary keys of its FROM and TO nodes
Through a random UUID property you must add
13. What is the role of the primary key in a node table?
The primary key uniquely identifies each node, is guaranteed non-null, and gets an index for fast lookups. Supported types include STRING, numeric types, DATE and BLOB.
If you do not have a natural key, use SERIAL to get auto-incremented integers. The key also drives bulk loads: the first two columns of a rel CSV are the primary keys of the source and destination nodes.
CREATE NODE TABLE Book(id SERIAL PRIMARY KEY, title STRING);
Choose a key that is stable and compact, because it is how rel files and patterns like MATCH (u:User {name: 'Adam'}) find nodes. Duplicate keys are rejected, which is the uniqueness guarantee you get in place of a separate constraint.
Take quiz
UUID_AUTO
COUNTER_ROW
MAP
SERIAL
Primary keys of the FROM and TO nodes
Edge labels
Timestamps of load
Internal page numbers
14. How do you import CSV data into LadybugDB?
Use COPY FROM, the recommended bulk path. Load node tables first, because every endpoint in a rel file must already exist.
COPY User FROM "user.csv" (header=true); COPY Follows FROM "follows.csv"; COPY User FROM "User*.csv"; -- glob COPY User FROM ["u0.csv", "u1.csv"]; -- explicit list
Compressed *.csv.gz files are read directly. Wrap strings in quotes and avoid padding spaces: an integer like 213 is treated as malformed and becomes NULL.
A typical load script creates the schema, runs COPY for each node table, then for each rel table. Options go in parentheses, such as (header=true, delim='|', ignore_errors=true). Files are read in parallel by default, and SPILL_TO_DISK lets an import continue when memory runs short.
Take quiz
Rels are stored inside node pages
Rel rows reference existing node primary keys
Node files compress rel files
COPY FROM refuses to run twice
No, decompress first
Only for rel tables
Yes, *.csv.gz is supported
Only in in-memory mode
15. List the client APIs available for LadybugDB?
Ladybug ships official bindings for Python, Node.js, Java, Rust, Go, Swift, C++ and C, plus the CLI shell. Ruby and Nim bindings are community maintained, and a WebAssembly build (Ladybug-Wasm) runs in the browser.
| Language | How to get it |
| Node.js | npm install @ladybugdb/core |
| Rust | cargo add lbug |
| Go | go get github.com/LadybugDB/go-ladybug |
| Java | Maven Central, com.ladybugdb:lbug |
| C/C++, CLI | Precompiled binaries from the GitHub releases |
All bindings drive the same engine, so schema, Cypher and file formats behave identically across languages. Pick the language of your host application, use the CLI for quick exploration, and use Ladybug Explorer when you want to browse the graph visually.
Take quiz
Python and Node.js
Java and Go
C and C++
Ruby and Nim
@ladybugdb/core
ladybug-server
lbug-cli
@neo4j/embedded
16. What is the LOAD FROM clause?
LOAD FROM scans an external source and returns its rows to a query without storing them. It works on more than CSV, which is why it replaced Neo4j's LOAD CSV FROM. Column names and types are inferred from the file header.
LOAD FROM 'user.csv.gz' RETURN *; LOAD FROM 'user.csv' (header=true) WHERE age > 30 RETURN name;
It is also the engine behind CREATE NODE TABLE ... AS LOAD FROM ..., which infers a schema and loads in one statement.
Combine it with WHERE and RETURN to preview a file, or see the Copy from subquery page when you need to reshape rows before they land in a table. Compressed .csv.gz files work too.
Take quiz
Yes, it always creates a table
No, it only scans and returns rows
Yes, but only for Parquet
It writes them to the WAL
A mandatory XML sidecar
The previous COPY command
Inferred from the file header and data
They are always STRING
17. What are relationship multiplicities in LadybugDB?
Multiplicity limits how many edges a node can have in each direction. The default is many-to-many. You can cap a side at one edge with MANY_ONE, ONE_MANY or ONE_ONE.
CREATE REL TABLE LivesIn(FROM User TO City, MANY_ONE); CREATE REL TABLE Likes(FROM Pet TO User, ONE_MANY);
MANY_ONE means each user lives in at most one city while a city can have many users. "Exactly one" semantics, like a foreign key, are not supported yet.
Use multiplicity when your domain has a natural cardinality, such as one city per user. It records that intent in the schema. Since exactly-one is not available yet, enforce mandatory links in your loading code.
Take quiz
Each city has at most one user
Each user must have exactly one city
Only one LivesIn edge exists overall
Each user has at most one city
Many-to-many
One-to-one
Many-to-one
Exactly-one
18. How do you run a parameterized query in LadybugDB?
Use $name placeholders in Cypher and pass a dictionary of values. Parameters keep values out of the query text, avoid string-building mistakes, and let you reuse the same statement shape.
conn.execute( "MATCH (b:Book {title: $title}) RETURN b.published_year", {"title": "The Quantum World"} )
The same style is used for inserting vectors: CREATE (b:Book {title: $title, title_embedding: $embeddings}) with a Python list as the value.
Parameters also carry lists, which is how embeddings are inserted without building giant strings. Other client APIs expose prepared statements as well, so check the prepared statements page for your language.
Take quiz
?title
$title
:title
{{title}}
It disables transactions
It forces a full table scan
Values never get concatenated into the query string
It skips type checking
19. What is the purpose of the CHECKPOINT statement?
CHECKPOINT merges the write-ahead log into the main database file and lets Ladybug discard the WAL. It runs automatically at the end of a write transaction once the WAL passes CHECKPOINT_THRESHOLD (16 MB by default) and no transactions are active.
CHECKPOINT; CALL checkpoint_threshold=33554432;
Calling it manually only works when nothing else is active; with an open transaction it errors, because forced checkpoints are not supported.
A larger threshold means fewer, bigger checkpoints and more data accumulating in the WAL between merges; a smaller one keeps the log short at the cost of more frequent merges. Set it with CALL checkpoint_threshold=<bytes>.
Take quiz
1 KB
16 GB
There is no automatic checkpoint
16 MB
Other transactions are still active
The database is too small
The WAL is empty only
Cypher forbids the keyword
20. What are LadybugDB extensions?
Extensions are optional modules you download and load on demand to add features without bloating the core. The flow is always INSTALL once, then LOAD per session.
INSTALL vector; LOAD vector;
Official ones are fetched into ~/.lbug/extensions. Available families include vector and fts search, algo graph algorithms, httpfs, S3, GCS and Azure storage, json, Iceberg, Delta and Unity Catalog, llm embeddings, and relational attachments for Postgres, DuckDB, SQLite and ADBC.
Extensions also reach remote data: httpfs reads over HTTPS, and the cloud storage extensions cover S3, GCS and Azure, so you can COPY FROM a remote file once credentials are configured.
Take quiz
/etc/ladybug/modules
~/.lbug/extensions
The database's .wal file
A temp folder deleted on exit
ENABLE vector;
IMPORT vector;
INSTALL vector; LOAD vector;
ATTACH vector;
21. What is the difference between COPY FROM and LOAD FROM?
COPY FROM bulk-writes file data into an existing table. LOAD FROM only reads a source into a query result, so you can filter, join or transform before deciding what to store.
| COPY FROM | LOAD FROM |
| Writes into a node or rel table | Returns rows, stores nothing |
| Target table must exist | No target table needed |
| Best for large initial loads | Best for inspection and ad hoc transforms |
They share the same CSV options such as header, delim and null_strings.
Rule of thumb: explore with LOAD FROM, commit with COPY FROM.
Take quiz
LOAD FROM
SHOW FROM
ATTACH FROM
COPY FROM
Previewing and filtering rows from a file
Creating the WAL
Changing CHECKPOINT_THRESHOLD
Dropping indexes
22. How does LadybugDB handle transactions?
Every query, DDL statement and COPY FROM runs in a transaction, and Ladybug is atomic, durable and serializable. After a commit your changes survive a crash; before a commit, a crash leaves none of them behind.
You can wrap statements yourself or let each one auto-commit. At any moment there can be many read transactions but only one write transaction.
BEGIN TRANSACTION; CREATE (a:User {name: 'Alice', age: 72}); MATCH (a:User) RETURN *; COMMIT;
Because transactions are serializable, readers see a consistent view while the single writer works. Use BEGIN TRANSACTION READ ONLY when you need a stable multi-query read. Keep manual transactions short and always end them with COMMIT or ROLLBACK; a crash in the middle of one discards everything it did.
Take quiz
As many as CPU cores
One
Two per connection
Unlimited with optimistic locking
All changes persist
Half the statements persist
None of its changes persist
It resumes automatically on restart
23. What is the difference between BEGIN TRANSACTION and BEGIN TRANSACTION READ ONLY?
BEGIN TRANSACTION starts a read-write transaction; BEGIN TRANSACTION READ ONLY starts one that may not write. Read-only transactions are useful for two reasons.
- You can run several queries and be sure the data does not change between them.
- You do not occupy the single write slot, so a writer elsewhere is not blocked.
End either with COMMIT, or use ROLLBACK to discard the changes of a read-write transaction.
Example: a report that runs five MATCH queries should open one read-only transaction so all five see identical data even if another connection commits meanwhile. Do not leave transactions open, because an active transaction prevents automatic checkpoints.
Take quiz
To skip the WAL for faster inserts
To enable multiple writers
To drop tables safely
To read consistently without blocking the writer
ROLLBACK
CHECKPOINT
RETURN
DETACH
24. What is the difference between READ_WRITE and READ_ONLY database modes?
The mode is chosen when you create the Database object. A READ_WRITE database can run reads and writes (CREATE, COPY FROM); a READ_ONLY database can only read.
The rule that matters: you may have one READ_WRITE object, or several READ_ONLY objects, never a mix on the same file. A writer can change disk layout, and other objects' buffer-manager caches would not know.
| Setup | Safe? |
| One READ_WRITE object, many connections | Yes |
| Many READ_ONLY objects, even across processes | Yes |
| READ_WRITE plus READ_ONLY objects | No |
In code, readers use lb.Database("example.lbdb", read_only=True). Prefer one shared Database per process with one Connection per thread; connections from the same READ_WRITE Database can issue concurrent reads and writes safely.
Take quiz
One READ_WRITE plus one READ_ONLY
Several READ_ONLY Database objects
Two READ_WRITE objects
A READ_WRITE object in each of two processes
Cypher forbids two MATCH queries
The WAL is single-threaded text
Their cached pages could become stale
Licenses restrict it
25. How do you troubleshoot the error Could not set lock on file?
This error means another Database object already holds the file lock. The classic case is a Jupyter notebook that built the graph while you start the CLI on the same .lbdb file.
RuntimeError: IO exception: Could not set lock on file : /path/to/example.lbdb
- Find the process that opened the file (notebook kernel, script, Explorer).
- Restart the Jupyter server or close that process so its Database object is destroyed.
- Reopen from one place only, or open every process
READ_ONLY. - For Ladybug Explorer in Docker, the lock flags are not visible across the container boundary, so stop the other process first.
Also look for a second terminal running lbug on the same path. To avoid the problem for good, open analysis tools read-only or send all writes through a single service process.
Take quiz
A corrupted CSV
A missing extension
A full tmp disk only
Another process already has the database open read-write
Restart the notebook server to release it
Delete the .wal file
Rename the table
Change CHECKPOINT_THRESHOLD
26. What is the difference between LadybugDB and Neo4j pattern matching semantics?
Neo4j applies trail semantics inside one MATCH: no relationship is repeated. LadybugDB applies walk semantics by default, so the same edge can be traversed more than once.
You can still ask for stricter behavior. Inside a recursive pattern add TRAIL (all relationships distinct) or ACYCLIC (all nodes distinct), or test a returned path with is_trail(p) and is_acyclic(p).
MATCH (a:User)-[e:Follows* trail 4..4]-(b:User) WHERE a.name = 'Zhang' RETURN b.name;
This explains why a query ported from Neo4j can return extra rows in Ladybug.
When porting a query, compare results on a small graph. If you relied on trail behavior to avoid revisiting edges, add TRAIL explicitly. The default walk semantics are also why unbounded recursion is capped at 30 hops.
Take quiz
Trail, no repeated edges
Walk, repeated edges allowed
Acyclic, no repeated nodes
Shortest path only
WALK
LOOPFREE
TRAIL
UNIQUE_NODES
27. What are the main Cypher syntax differences between LadybugDB and Neo4j?
Ladybug follows openCypher closely but deliberately omits or renames some things. The ones that bite most often when porting queries:
| Neo4j | LadybugDB |
FOREACH |
Not supported, use UNWIND |
REMOVE n.prop |
SET n.prop = NULL |
labels(n) |
label(n) |
MATCH (n:Person WHERE n.age > 3) |
Move the filter to a normal WHERE |
SHOW FUNCTIONS |
CALL show_functions() RETURN * |
elementId(n) |
id(n) |
CALL { ... } subquery |
Not supported; EXISTS and COUNT subqueries are |
Also remember that a schema must exist before inserting data.
Types differ too: Ladybug follows Postgres typing, lists must hold one element type, and cosine similarity is ARRAY_COSINE_SIMILARITY(). Spatial functions, isNaN, e() and pi() are missing, and date() becomes current_date().
Take quiz
DELETE n.prop
DROP PROPERTY n.prop
UNSET n.prop
SET n.prop = NULL
label()
tags()
kind()
types()
28. How do you find shortest paths in LadybugDB?
Put a keyword right after the star in a variable-length relationship. The lower bound must be 1, and an upper bound should always be given.
| Pattern | Meaning |
-[e* SHORTEST 1..4]-> |
One shortest path per destination |
-[e* ALL SHORTEST 1..3]- |
All shortest paths |
-[e:Follows* WSHORTEST(score)]-> |
Cheapest path by a numeric rel property |
-[e:Follows* ALL WSHORTEST(score)]-> |
All cheapest paths |
MATCH p=(a:User)-[e:Follows* WSHORTEST(score)]->(b:User) WHERE a.name = 'Adam' RETURN properties(nodes(p), 'name'), cost(e);
Prefer SHORTEST when you only need reachability or length, since paths do not have to be materialized.
Drop the arrow to ignore direction, as in -[e* SHORTEST 1..4]-. The docs recommend plain SHORTEST when you do not need the paths themselves, and ask you to keep upper bounds modest on dense graphs.
Take quiz
weight(e)
cost(e)
length(e) only
score_sum(p)
0
2
1
Any value
29. How do you filter relationships inside a variable-length pattern?
Add a predicate group after the hop range: (r, n | WHERE ...). The first variable binds each intermediate relationship, the second each intermediate node.
MATCH p = (a:User)-[:Follows*1..2 (r, n | WHERE r.since < 2022 AND n.age > 45)]->(b:User) WHERE a.name = 'Adam' RETURN b.name, COUNT(*);
Predicates may use only the relationship, only the node, or an AND of both kinds. An OR mixing node and relationship properties is not supported. You can also project just the properties you need, {r.since}, {n.name}, to cut memory use.
A projection example is -[e:Follows*1..2 (r, n | WHERE r.since > 2020 | {r.since}, {n.name})]->. Only direct property projections are allowed, not expressions. It helps when paths are long and wide nodes would otherwise be copied into every intermediate result.
Take quiz
Source and destination node
Start and end timestamp
Left and right join side
An intermediate relationship and an intermediate node
n.age > 45 OR r.since < 2022
r.since < 2022
n.age > 45
n.age > 45 AND r.since < 2022
30. What happens when a variable-length relationship has no upper bound?
Ladybug fills in a default maximum of 30 hops. This is needed because walk semantics allow repeated edges, so an unbounded pattern on a cyclic graph might never terminate.
You can change the default for a connection with var_length_extend_max_depth, but a tight explicit bound in the query is better for both speed and result size.
CALL var_length_extend_max_depth=10; MATCH (a:User)-[e:Follows*]->(b:User) RETURN count(*);
A good habit is to write the bound you really need, such as *1..3 for friends of friends of friends. A bound of 30 on a dense graph can explode result counts. Adding TRAIL or ACYCLIC also removes redundant paths, and CALL TIMEOUT=3000 stops a runaway query after three seconds.
Take quiz
10
30
255
Unlimited
Because indexes are limited to 30 pages
Because rel tables hold 30 rows
Cycles could otherwise make the traversal endless
Because Cypher forbids stars
31. How do you create and query a vector index in LadybugDB?
Load the vector extension, store embeddings as FLOAT[n] or DOUBLE[n] properties on a node table, then build an HNSW index with CREATE_VECTOR_INDEX.
INSTALL vector; LOAD vector; CALL CREATE_VECTOR_INDEX('Book', 'book_title_index', 'title_embedding', metric := 'l2'); CALL QUERY_VECTOR_INDEX('Book', 'book_title_index', $query_vector, 2) RETURN node.title, distance ORDER BY distance;
The result gives node and distance, so you can continue straight into a MATCH to traverse from the hits. Only node-table properties can be indexed. Use DROP_VECTOR_INDEX to remove one.
Tuning knobs are mu, ml, efc (build) and efs (search), defaulting to 30, 60, 200 and 200. Metrics are cosine, l2, l2sq and dotproduct. Embeddings can come from a Python library such as sentence-transformers or from the llm extension's create_embedding. Run CALL SHOW_INDEXES() RETURN * to confirm the index exists.
Take quiz
Only a boolean
A CSV path
The raw embedding file
A node and its distance
On a node table property
On a relationship property
In an external Parquet file only
In the WAL
32. How do you run full-text search in LadybugDB?
Load the fts extension, create an index over one or more STRING properties of a node table, then query it. Scoring uses Okapi BM25.
CALL CREATE_FTS_INDEX('Book', 'book_index', ['abstract', 'title'], stemmer := 'porter'); CALL QUERY_FTS_INDEX('Book', 'book_index', 'quantum machine', conjunctive := false, top := 3) RETURN node.title, score ORDER BY score DESC;
Use conjunctive := true when every keyword must appear. K and B tune term-frequency saturation and length normalization. Custom stopwords can come from a node table or a CSV or Parquet file, and changing them requires rebuilding the index. CREATE_FTS_INDEX cannot share a multi-statement block with other statements.
List indexes with CALL SHOW_INDEXES() and remove one with DROP_FTS_INDEX. Stemmers cover many languages, from the default english Snowball and porter to French, German and Hindi; use none to turn stemming off.
Take quiz
PageRank
Okapi BM25
Levenshtein distance only
TF-only counting
Returns every row
Disables stemming
Requires all query keywords to appear
Sorts results alphabetically
33. How do you run graph algorithms in LadybugDB?
Install and load the algo extension. Algorithms run on a projected graph, a named subset of node and rel tables, optionally with filters. Available algorithms are PageRank, Weakly and Strongly Connected Components, Louvain and K-Core.
INSTALL algo; LOAD algo; CALL PROJECT_GRAPH('G', ['Person'], ['KNOWS']); CALL SHOW_PROJECTED_GRAPHS(); CALL DROP_PROJECTED_GRAPH('G');
A projected graph is not materialized: data is scanned from disk when an algorithm runs. It lives until you drop it or close the connection. Graphs stay directed, though WCC ignores direction.
Filters in a projection use n for nodes and r for relationships, for example {'KNOWS': 'r.id < 3'}, and each predicate may touch only its own table. With the Python async API a projected graph can seem to vanish, because queries may run on different pooled connections; create and use it on the same connection.
Take quiz
Only the whole database implicitly
A CSV export
A materialized in-memory copy
A projected graph
Until dropped or the connection closes
Forever, stored in the .lbdb file
Exactly one query
Until the next CHECKPOINT
34. Which external relational databases can LadybugDB scan from?
Through extensions, Ladybug can scan data with LOAD FROM from PostgreSQL, DuckDB and SQLite, and from any database with an ADBC driver. You can read the data in place or copy it into node and rel tables.
| Extension | Source |
postgres |
Attached PostgreSQL database |
duckdb |
Attached DuckDB database |
sqlite |
Attached SQLite database |
adbc |
Any ADBC-compatible database |
Prefer a dedicated extension when one exists: it supports extras such as SQL_QUERY pass-through, which the generic ADBC adapter does not.
Other sources work in a similar way: Iceberg, Delta Lake and Unity Catalog for lakehouse data, and S3, GCS or Azure for files in cloud storage. Scanning in place keeps one source of truth, while COPY ingests once for faster graph queries.
Take quiz
jdbcx
adbc
sqlbridge
rdbms_any
Free cloud hosting
Automatic sharding
SQL_QUERY pass-through
A GUI schema editor
35. How do you skip malformed rows during a CSV import?
Set IGNORE_ERRORS=true in the COPY FROM options. Bad rows are skipped instead of aborting the load, and you can inspect what was skipped afterwards.
COPY User FROM "user.csv" (header=true, ignore_errors=true); CALL SHOW_WARNINGS() RETURN *; CALL CLEAR_WARNINGS(); CALL warning_limit=1024;
Each connection stores at most WARNING_LIMIT warnings (8192 by default). Typical culprits are values with stray padding, such as 213 in an integer column.
A sensible workflow is to import with ignore_errors, check SHOW_WARNINGS, fix the source data, then CLEAR_WARNINGS before the next batch. Raise warning_limit first if you expect more than 8192 bad rows, and compare the final row count with what the file should contain. If a tool writes the text NULL for missing values, set null_strings too.
Take quiz
16
1 million
Unlimited
8192
SHOW_WARNINGS
SHOW_SKIPPED_FILES
LIST_BAD_ROWS
DUMP_ERRORS
36. How does CSV auto-detection work in COPY FROM?
When you do not specify them, Ladybug detects HEADER, DELIM, QUOTE and ESCAPE from the first sample_size lines (256 by default).
- Header: the first line is a header if its fields cannot be cast to the target column types.
- Delimiter: chosen from
,,|,;and tab. - Quote: double quote, single quote or none.
- Escape: double quote, single quote, backslash or none.
Anything you set manually is not re-detected. With auto_detect=false the defaults apply: comma, " and \.
If detection guesses wrong, override just the field that is wrong and the rest stays automatic. For files with unusual quoting, raise sample_size so the sample covers a representative section of the file.
Take quiz
2
256
10,000
The whole file
It starts with a hash sign
The file name contains header
Its fields fail to cast to the target column types
It has fewer than three fields
37. How does CREATE NODE TABLE AS work?
It creates a table and fills it from a subquery in a single statement, inferring the schema from the subquery's output. The source can be a file or a MATCH.
CREATE NODE TABLE Person AS LOAD FROM "person.csv" RETURN *; CREATE NODE TABLE YoungPerson AS MATCH (p:Person) WHERE p.age < 25 RETURN p.*; CREATE REL TABLE Knows(FROM Person TO Person) AS LOAD FROM "knows.csv" RETURN *;
Add IF NOT EXISTS to make it re-runnable. For rel tables you still name the FROM and TO tables.
This is handy for prototypes and ETL: derive a table from a file, or materialize a filtered copy of an existing table. For production schemas, an explicit CREATE NODE TABLE gives you control over keys, defaults and exact types, because inferred types come from the data. Create node tables before building a rel table from a file of endpoint keys.
Take quiz
Only the table name
Indexes on all columns
The WAL size
The schema from the subquery's output
A MATCH query
A running REST endpoint
Only another CSV
A browser cookie
38. What is an open-type graph in LadybugDB?
Ladybug supports subgraphs that each have their own schema, data and configuration, similar to separate databases. By default they are strictly typed: every node table must be created first.
An open-type graph is created with CREATE GRAPH mygraph ANY. There you can create nodes with labels without declaring node tables first, which eases migration from GQL or Neo4j-style data.
CREATE GRAPH mygraph ANY;
Check the release notes for the current state of graph selection, since the Neo4j comparison page still says each graph is its own database.
Use the default strict mode when you control the schema and want mistakes to raise errors. Use ANY when you are importing loosely structured graph data and want to move quickly.
Take quiz
Anonymous users to connect
Creating labeled nodes without declaring node tables first
Skipping primary keys in rel files
Running without a WAL
Open type
Schemaless JSON
Strictly typed
Read-only
39. How does LadybugDB execute queries in parallel?
It uses morsel-driven parallelism. A pipeline can run on many threads, and the pipeline's source operator hands out small chunks of input to each thread.
For a CSV scan, the source first counts lines and then dispatches different line ranges to different threads. The thread count defaults to the system maximum and can be lowered per connection.
CALL THREADS=5; CALL TIMEOUT=3000;
Leave THREADS at the default for analytics, or lower it when Ladybug shares a machine with other work. Handing out small chunks dynamically keeps all cores busy even when data is skewed, because a thread that finishes early simply takes the next chunk. The threads belong to your application's process, and TIMEOUT in milliseconds protects against runaway queries. Imports benefit too, since COPY FROM scans CSV files in parallel by default.
Take quiz
The sink operator only
The client driver
The WAL writer
The pipeline's source operator
THREADS
MAX_CORES_TOTAL
WORKERS
PARALLEL_LIMIT
40. Explain the execution flow of a pipelined query in LadybugDB?
The physical plan is cut into pipelines: linear chains of operators. A source operator at the leaf scans storage or another pipeline's output, and a sink operator at the end accumulates results. Between source and sink, data flows without being materialized.
A new pipeline starts at every sink operator, meaning one that must exhaust its input first, such as HASH_JOIN_BUILD, AGGREGATE or ORDER BY. Pipelines can depend on each other: a hash join probe pipeline waits for its build pipeline.
flowchart LR A["Scan build side"] --> B["HASH_JOIN_BUILD sink"] C["Scan probe side"] --> D[HASH_JOIN_PROBE] B -. must finish first .-> D D --> E["AGGREGATE sink"] E --> F[Result]
Pipelining avoids writing intermediate tables between operators, so only sinks hold state. When profiling a slow query (see the Performance debugging page), look at where a sink forces a new pipeline, because that is where the query must wait for all input.
Take quiz
After every scan
At each sink operator
At every WHERE clause
Only at the final RETURN
ORDER BY on another table
The CHECKPOINT pipeline
HASH_JOIN_BUILD
A CSV header reader
41. How does LadybugDB store graph data for fast joins?
Storage is columnar and disk-based. Relationships are held in CSR (compressed sparse row) adjacency lists together with join indices, so expanding from a node to its neighbors reads a contiguous range instead of probing a hash table for every edge.
That matters because a graph pattern is a chain of joins. Ladybug can scan edges in the forward or backward direction, and the optimizer chooses the side that is cheaper given your filters. Columnar layout also lets scans touch only the properties a query needs, and vectorized operators process them in batches.
On the write side, the single-file format and the WAL keep updates durable while checkpoints fold them into the columnar files. The result behaves like an analytical column store with graph-aware indexes, rather than a pointer-chasing record store.
Take quiz
A single JSON blob per node
A global hash map in RAM only
Row-oriented heap files with pointers
CSR adjacency lists with join indices
Queries read only the columns they need
It removes the need for a schema
It disables the WAL
It forces single-threaded scans
42. What is factorized query processing and why does it matter for graph queries?
Factorization is a technique where intermediate results are kept as groups instead of fully flattened tuples. Ladybug's docs list it, along with vectorization, among its core execution features.
Consider a person with 1,000 friends, each with 1,000 friends of their own. A flat join writes one tuple for every pair. A factorized form stores the person once, then the friend list, then each friend's list, and expands combinations only when a result is actually needed.
Multi-hop expansions often blow up row counts, so avoiding that repetition saves memory and CPU on join-heavy workloads.
You rarely write anything special to get this; it is an execution strategy chosen by the engine. It pairs with vectorized processing, which handles values in batches, and with worst-case optimal joins for cyclic patterns, to keep multi-hop queries tractable.
Take quiz
Writing to the WAL
Repeating shared values across flattened join tuples
Parsing Cypher
Using indexes
Single-row primary key lookups
CREATE NODE TABLE statements
Multi-hop expansions with large fan-out
CHECKPOINT calls
43. When should you use the HINT clause in LadybugDB?
Use HINT when the optimizer picks a poor join order, or when you want to experiment with plans. It is optional and scoped to a single connected MATCH pattern; every node and relationship variable must be named and appear exactly once.
MATCH (a)-[e]->(b) WHERE b.ID = 0 HINT a JOIN (e JOIN b) RETURN *;
The parentheses form a binary join tree. Here (e JOIN b) starts from the filtered b and scans edges backward. In a hash join the left side is the probe and the right is the build side, so swapping sides can change performance.
Do not hint by default, since the optimizer usually does well. Reach for HINT after profiling shows a bad plan, for instance when a selective filter sits at one end of the pattern. The pattern must be connected, so MATCH (a), (b) with no link cannot be hinted. Relationship variables may be recursive, such as -[e*1..3]->.
Take quiz
A flat comma list
A JSON object
An index name
A binary tree written with parentheses
The right side
The left side
Neither, hash joins are not allowed
Always the larger table
44. How does MULTI_JOIN enable worst-case optimal joins in LadybugDB?
For cyclic patterns like triangles, MULTI_JOIN forces a worst-case optimal join (WCOJ) operator. Instead of joining edge by edge, it fetches adjacency lists from several edges and intersects them on the shared node.
MATCH (a:person)<-[e1:knows]-(b:person)-[e2:knows]->(c:person), (a)-[e3:knows]->(c) HINT (((a JOIN e1) JOIN b) MULTI_JOIN e2 MULTI_JOIN e3) JOIN c RETURN COUNT(*)
For each (a, e1, b) tuple, the operator takes the lists for e2 and e3 and sort-merge intersects them to find c. You need at least two edge variables; with one, MULTI_JOIN degrades to a plain JOIN.
Binary joins can create huge intermediate results on triangles and cliques because they match two edges before checking the third. WCOJ avoids this by binding one node at a time through intersection. The shape rule is k tree nodes, where k-1 are edge variables meeting at one node c, plus a probe node that can be any sub-plan. Placing c on either side of the final JOIN chooses the build and probe sides.
Take quiz
Concatenates them in file order
Intersects them on the shared node
Hashes them into a Bloom filter only
Copies them to the WAL
It raises a syntax error
It runs a WCOJ anyway
It is treated as a regular JOIN
It doubles the thread count
45. Explain the lifecycle of a write transaction in LadybugDB?
A write transaction logs its changes to the write-ahead log and, on commit, those records are durable in the .wal file. The main .lbdb file is updated later by a checkpoint.
BEGIN TRANSACTION(or an auto-wrapped statement) takes the single write slot.- Changes are recorded in the WAL.
COMMITmakes them durable;ROLLBACKdiscards them.- If the WAL exceeds
CHECKPOINT_THRESHOLDand no transactions are active, a checkpoint merges it into the database file using a temporary.shadowfile. - On success the WAL and shadow files are removed.
flowchart TD
A[BEGIN] --> B["Write to WAL"]
B --> C{COMMIT?}
C -- yes --> D["Durable in WAL"]
C -- no --> E["Rollback, discard"]
D --> F{WAL over threshold and idle?}
F -- yes --> G["Checkpoint via shadow file"]
G --> H["WAL removed"]
Failure handling follows from this order. A crash before commit leaves nothing behind; a crash after commit but before checkpoint still keeps the change, because the committed WAL record is durable. While a checkpoint runs, the shadow file provides a safe path, and a manual CHECKPOINT errors if any transaction is still active.
Take quiz
At every statement
Only on process start
When the tmp file is deleted
At a checkpoint
.shadow
.lock
.idx
.snapshot
46. How can you support multiple read-write processes on one LadybugDB database?
You cannot open several read-write Database objects on the same file, so route all writers through one process. The documented pattern is an API server that owns a single READ_WRITE Database and serves many clients over HTTP or gRPC.
flowchart LR C1["Client 1"] --> S["API server process"] C2["Client 2"] --> S C3["Client 3"] --> S S --> D[(One READ_WRITE Database)]
Concurrent connections inside that server are safe; its transaction manager serializes writes. A REST-style server powered by Express.js is provided. The cost is that results travel as JSON over the network, so embedding Ladybug directly is faster when you can.
Do not point a notebook and a CLI at the same file in read-write mode; the lock error is the warning sign. Read-only analysts can open the file only when no writer process exists. If you need both at once, expose the data through the server instead. Ladybug Explorer likewise should not run against a file held by another READ_WRITE process.
Take quiz
Each client opens its own READ_WRITE object
One server process owns a single READ_WRITE Database
Mixed READ_ONLY and READ_WRITE objects
Copy the file per client and merge later
Transactions are disabled
Cypher is unavailable
Query results pass as JSON over the network
Only CSV import works
47. How does filtered vector search work in LadybugDB?
You build a projected graph that keeps only the nodes passing a predicate, then pass that graph's name as the first argument of QUERY_VECTOR_INDEX instead of the table name.
CALL PROJECT_GRAPH('filtered_book', {'Book': 'n.published_year > 2010'}, []); CALL QUERY_VECTOR_INDEX('filtered_book', 'book_title_index', $query_vector, 2) WITH node AS n, distance AS dist MATCH (n)-[:PublishedBy]->(p:Publisher) RETURN n.title, p.name ORDER BY dist;
For arbitrary patterns use PROJECT_GRAPH_CYPHER, whose query must return one node variable of the indexed table. Graphs made that way work only for filtered vector search, not with the algo extension.
Projected graphs are not materialized, so defining one is cheap. Predicates use n as the node placeholder and may touch only their own table. You can chain the hits into a MATCH for hybrid search: vector similarity finds entry points, then graph traversal adds context, such as the publishers of the most similar books. Keep the filter predicate simple, because it must reference only the projected table's own properties.
Take quiz
A WHERE string appended to the query vector
The CSV path
An FTS index name
The name of a projected graph
The algo extension
Filtered vector index search
SHOW_INDEXES
A node-returning MATCH
48. How do mu, ml, efc and efs affect HNSW tuning in LadybugDB?
Ladybug's vector index is a disk-based HNSW graph with two layers: the lower holds all vectors, the upper a sampled subset. Build-time and search-time knobs trade accuracy against cost.
| Parameter | Effect | Default |
mu |
Max degree in upper layer; keep below ml |
30 |
ml |
Max degree in lower layer | 60 |
pu |
Share of nodes sampled into the upper layer | 0.05 |
efc |
Candidates considered while building | 200 |
efs |
Candidates considered while searching | 200 |
metric |
cosine, l2, l2sq or dotproduct | cosine |
Higher values improve recall but enlarge the index and slow builds or queries. cache_embeddings := true speeds construction at the cost of memory.
Start with the defaults, measure recall against exact search on a sample, then raise efs first, because a search-time change needs no rebuild. Only then rebuild with a larger ml or efc. Pick the metric that matches how your embedding model was trained.
Take quiz
efc
efs
pu
mu
Lower recall
Fewer supported metrics
Slower, larger index construction
It disables cosine distance
49. How does LadybugDB handle larger-than-memory workloads?
Use on-disk mode. The docs state that on-disk databases process larger-than-memory workloads, because data lives in the columnar database file and pages are cached by the buffer manager rather than loaded wholesale.
When intermediate data is bigger than available memory, Ladybug spills it to the .tmp file and removes it when the query finishes. For bulk imports, SPILL_TO_DISK (true by default) lets COPY FROM do the same.
CALL spill_to_disk=false; -- cannot be TRUE in in-memory or read-only mode
In-memory databases cannot spill, so keep them for small datasets.
Give the buffer pool enough room (the Go example sets BufferPoolSize in its system config), keep the database on fast local storage, and reduce THREADS if memory is tight, because every thread needs working memory for its pipeline. If an import is the heavy part, keep SPILL_TO_DISK at its default of true.
Take quiz
In-memory mode
Wasm mode only
READ_ONLY in-memory mode
On-disk mode
A temporary .tmp file beside the database
The .lbdb file itself
The extension cache
history.txt
50. When would you choose LadybugDB over Neo4j?
Choose Ladybug when you want an embedded engine for join-heavy analytics: knowledge graphs inside an app, recommendation or relationship analysis, notebooks, edge or in-browser use via Wasm. It needs no server and reads Parquet, Arrow, DuckDB and more.
| Pick Ladybug when | Pick Neo4j when |
| You want in-process, serverless embedding | You need a managed, networked service |
| Analytics over large graphs dominate | You rely on its wider ecosystem and tooling |
| A fixed schema is acceptable | You need schemaless, ad hoc labels and custom indexes |
| One writer process is enough | Many clients write to one server |
Remember Ladybug limits: one writer at a time, no user-defined secondary indexes beyond primary keys and the vector and FTS extensions, and a few Cypher gaps.
A quick way to decide is to prototype your top three queries and your write pattern on both systems. Ladybug's Neo4j differences page lists exactly what needs rewriting.