TypeDB 3.0 is live! Get started for free.

Crash Course

TypeDB brings together powerful programming concepts with the features and performance of modern database systems. We believe the result is a novel database experience - and after taking this 15-minute crash course, we hope you’ll agree.

Storing data in types

In TypeDB, data is stored in types, which can be either

  • entity types (e.g., a type of all the users in your app)

  • relation types (e.g., all the friendships of users)

  • attribute types (e.g., the usernames of users, or the start_dates of friendships)

Data objects (instances) may reference other instances. A relation references other entities or relations, which we call its role players. An attribute is owned by a single entity or relation, which we call its owner. Makes sense, right?

TypeDB’s type system unifies concepts from relational, document, and graph DBMSs, blending them with a modern programming paradigm. Let’s see how this plays out for writing queries.

“Query programming” with TypeDB

The query language of TypeDB is TypeQL. It combines declarative and functional programming concepts with the simplicity of natural language. Here’s a simple TypeQL query to get us started:

match
  $alex isa user, has username "grothendieck_25";
  $friendship isa friendship (friend: $alex, friend: $friend-of-alex);
reduce $alex-friend-count = count;

This query returns the count of how many friends the user $alex has (identified by their username "grothendieck_25"). Note that the query works in two stages:

  1. First, the match stage outputs all answers from the database that satisfy the statements in the body of the stage. This means we output all possible data combinations from the database for the three used variables!

  2. Second, the reduce stage counts these answer combinations, outputs a single number, and assigns it to the variable $alex-friend-count.

The pattern $var isa <type> means the variable $var represents a data object (instance) of type <type>. An instance may be independent, or it may reference other instances.

TypeQL has special statements to capture references between instances, like $relation isa <type> (<role>: $player); or $owner has <type> $attribute.

Here’s an example of a slightly more complex query.

match
  $alex isa user, has username "grothendieck_25";
  friendship (friend: $alex, friend: $friend-of-alex); # can omit variable
reduce $alex-friend-count = count;
match
  $other-user isa user, has username $other-name;
  friendship (friend: $other-user, friend: $friend-of-other-user);
reduce $other-friend-count = count
  groupby $other-name, $alex-friend-count;
match
  $alex-friend-count < $other-friend-count;
select $other-name;

The goal of this query is to find users with more friends than Alex. It has 6 stages:

  • Stage 1 and 2: Count Alex' friends - and assign the output to $alex-friend-count.

  • Stage 3 and 4: Count all other users' friends. Match all $other-users with their respective usernames, $other-name, and their friends. Group the answers by unique combinations of $other-name and $alex-friend-count. Finally, count them.

  • Stage 5 and 6: List users with more friends than Alex. Introduce a condition - $alex-friend-count < $other-friend-count - and return only the results that satisfy it. select the username: $other-name.

Isn’t that neat? Combining multiple query stages into a single query pipeline is powerful. However, the resulting query now has quite a bit of repetitive code. Let’s optimize it using a helper function.

(Don’t worry. This is not an exercise for the reader. We’ve optimized it for you.)

with fun friend_count($user: user) -> integer:
  match friendship (friend: $user, friend: $friend-of-user);
  return count;
match
  $alex isa user, has username "grothendieck_25";
  $other-user isa user, has username $other-name;
  friend_count($alex) < friend_count($other-user);
select $other-name;

We’ve just defined a helper function using the fun keyword that abstracts a key part of our query logic. Functions use the same syntax as queries, so they’re easy to write.

Defining your type schema

The types user and username in the above queries don’t come out of nowhere: in order to query data in types, you first need to define them. Types are defined with define queries like so:

define
  entity user, owns username, owns status;
  attribute username, value string;
  attribute status, value string;

Similarly, if we were to add a friendship type to our schema we would define:

define
  relation friendship, relates friend @card(2);
  user plays friendship:friend;
  • Line 1: Introduce the relation type friendship with a friend role (programmer-lingo: think “trait” that can then be “implemented” by other types). Declare that each friendship requires exactly two friends (card is short for “cardinality”, and is a first example of a annotation: annotations make schema statements more specific - more on that later).

  • Line 2: Users can play the role of friends in friendships (more programmer-lingo: the type user implements the “friend-role” trait).

Relation types, like entity types, are types of “first-class objects” in our type system. In particular, they too can have attributes (or may play roles in other relation types).

define
  attribute start-date, value datetime;
  friendship owns start-date @card(0..1);

This defines a start-date attribute and then declares each friendship may own a start_date (yet more programmer-lingo: the type friendship implements the “start-date-owner” trait). Moreover, the @card annotation specifies that friendships own between zero or one start dates. This is the default cardinality, so you don’t actually need @card here at all.

The next example illustrates usage of “unbounded” cardinality together with another useful annotation, @key (which, in this case, specifies that the username of a user should uniquely identify a user, i.e., it is a key attribute).

define
  user owns status @card(0..);  # as many status attributes as you wish
  user owns username @key;

Importantly, different types may own the same attributes; and, similarly, different types may play the same role of a relation type. This flexibility of connecting types is a central feature of data modeling with TypeDB.

define
  entity organization,
    owns username,
    owns status,
    plays friendship:friend;

Finally, yet another fundamental and powerful feature of TypeDB’s type system is subtyping: analogous to modern programming language features, this gives us the ability to organize our types hierarchically:

define
  entity company sub organization;    # subtypes will inherit ownership and role
  entity university sub organization; # playing traits from their supertype

TypeDB’s type system re-thinks data models from first principles: it modularizes schemas into their "atomic" components. For example, you can add or remove ownership at any point in time, or edit specific annotations. This makes it easy to migrate and combine data, and programmatically re-structure your database if necessary. There is much more to explore, but we refer to the Schema Manual for further details.

CRUD operations

Having defined the types in our schema, we are ready to write data to our database. To begin, let’s insert a user with the following insert query:

insert $x isa user;

This works, but is pretty useless: it inserts an entity with type user, but gives no information about that entity (in fact, the query will fail if user owns username @key).

The following is more meaningful:

insert $x isa user, has username "user_0";

We can insert multiple values in a single insert query like so:

insert
  $x isa user, has username "user_1";
  $y isa user;
  $y has username "user_2";  # we can separate `isa` and `has`

This inserts two users at the same time. Recall, that we set usernames to be a @key attribute of our users, so you would - rightfully - expect the following query to fail:

insert
  $x isa user, has username "user_3";
  $y isa user, has username "user_3";

And it does fail, because the key 'user_3' is now claimed by two distinct user instances.

So far so good. Next question: how do we insert friendships? We need to refer to users to instantiate our friendship (recall: each friendship takes two friends). That’s where variables come into play.

insert
  $x isa user, has username "grothendieck_25";
  $y isa user, has username "hartshorne";
  friendship (friend: $x, friend: $y); # alternatively: $z isa friendship (friend: $x, friend: $y);

And now we’ve inserted two new users with a friendship relation between them.

What if we want to create a friendship relation between two existing users already in the database? That’s a two-stage query pipeline - (1) retrieve them with match; (2) insert the new friendship.

match
  $u0 isa user, has username "user_0";
  $u1 isa user, has username "user_1";
  $u2 isa user, has username "user_2";
insert
  friendship (friend: $u0, friend: $u1);
  friendship (friend: $u0, friend: $u2);
  friendship (friend: $u1, friend: $u2);

Easy!

Deleting and updating data work in a similar vein: all data write operations can be used as pipeline stages, and they all use essentially the same declarative statement-by-statement syntax. For example, the following pipeline adds "VIP" to a user’s status, then traverses all friendships of that user, and marks friends as VIPs themselves if they now have more than five VIP friends.

match
  $user isa user, has username "user_0";
insert $user has status "VIP";
match
  friendship (friend: $user, friend: $friend);
  friendship (friend: $friend, friend: $friend-of-friend);
  $friend-of-friend has status "VIP";
reduce $VIP-friend-count = count groupby $friend;
match $VIP-friend-count > 5;
insert $friend has status "VIP";

See the CRUD manual for more.

Database management 101

The time has come to actually run our queries. Recall from the Quickstart, the first two steps of the TypeQB workflow are always as follows:

  1. Ensure that your TypeDB cluster is running.

  2. From the TypeDB client of your choice, establish a connection to the server.

Next up, let us create a database. The workflow here will depend on your client, but is easy in either case.

  • Console

  • Python

  • Rust

  • Studio

To list existing databases use:

$ database list

To create a new database called my-db use

$ database create my-db

To delete a database use

$ database delete my-db

After connecting with

driver = TypeDB.core_driver(address=address, credentials=Credentials(user,pw), driver_options=DriverOptions())

you can create a database with:

driver.databases.create(database)

and delete a database with:

driver.databases.get(database).delete()

After connecting with

let driver = TypeDBDriver::new_core(address, Credentials::new(user, pw), DriverOptions::new(false, None)?).await?;

you can create a database with:

driver.databases().create(db_name).await?;

and delete a database with:

driver.databases().get(db_name).await?.delete().await?;

After opening a project, use the database management button (studio dbs) to create and manage databases.

See the Studio Manual for more.

Running queries with transactions

In order to run our queries, the next step is to open a transaction for an existing database. We can run multiple queries in single a transaction, and then commit the transaction to persist our changes when done. Transactions are required in TypeDB - you can’t run a query without one. There are three types of transactions:

  • Schema transactions allow you to run schema definition queries (like the define queries that we’ve seen above).

  • Write transactions allow you to send query pipelines that may write data (i.e., they contain “write stages” like the insert or delete stages that we have seen above)

  • Read transactions allow you to send query pipelines that only read data (i.e., they contain only “read stages” like the match, reduce, or select stages that we have seen above).

How to open a transaction and run a query will depend on your client.

  • Console

  • Python

  • Rust

  • Studio

Open the transaction with

transaction my-db <TYPE>

where <TYPE> can be one of schema, write, or read.

Then run your queries in the newly opened prompt. Use

commit

if you want to commit changes.

Open a transaction and send a query with

with driver.transaction(database, TransactionType.<TYPE>) as tx:
    response = tx.query(<QUERY>).resolve()

where <TYPE> can be one of SCHEMA, WRITE, or READ. Use

    tx.commit()

if you want to commit your changes.

Open a transaction and send a query with

let tx = driver.transaction(db_name, TransactionType::<TYPE>).await?;
tx.query(<QUERY>).await?;

where <TYPE> can be one of SCHEMA, WRITE, or READ. Use

tx.commit().await?;

if you want to commit your changes.

Select your database using the database selector (database none), then select your transaction type in the top menu.

Open your query file. Then click the run button to run your query. Finally, use the commit button (studio check) to persist any changes made.

Do you take a hands-on approach to learning? If so, try running queries in this course in the following order:

  1. Define a schema as outlined in the schema section. Use schema transactions for your queries and don’t forget to commit any changes made.

  2. Insert data instances into the types of your schema as explained in the CRUD section. Use write transactions for your queries and don’t forget to commit any changes you want to persists.

  3. Read your data using query pipelines like those in the pipeline section.

Beyond the basics: functions and query composition

Coming soon.

What next?

Continue learning how to use TypeDB with TypeDB Academy, or explore other sections of the documentation.

An end-to-end learning experience for TypeDB and TypeQL, showing how to take advantage of TypeDB’s unique features.

Practice-oriented guides on using TypeDB, including the TypeDB Studio and TypeDB Console manuals.

Installation guides, tutorials, and API references for the official TypeDB drivers in all supported languages.

Complete language reference for TypeQL, covering all query types, pattern elements, and keywords.