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.

Database management 101

Before we get started, ensure that TypeDB is running as detailed in the Quickstart, and have the server’s address and user credentials at hand. Now, let’s connect to TypeDB and create your first database!

The workflow here will depend on your client, but is straight-forward in either case. If you are new to TypeDB, we recommend trying TypeDB via TypeDB Studio. But you can also access TypeDB via various language drivers and TypeDB’s command line interface, TypeDB Console, in this course.

First, ensure you have an active connection to TypeDB. If you don’t, all Studio pages will prompt you to connect to TypeDB using your address and credentials.

To select a database to work with, use the dropdown menu on the right of the database icon in the top toolbar. You can also create new databases here.

TypeDB Studio will automatically select a default database if there are no others present.

Working with transactions

In order to run our queries in TypeDB, you need to open a transaction for an existing database. Transactions are the "units of work" that we apply to our database: we can run multiple queries in single a transaction, and may commit the transaction to persist the changes done by its queries. Transactions are always required in TypeDB - you can’t run a query without one, even if your queries do not modify the database.

There are three types of transactions:

  • Schema transactions allow you to run any type of queries, including queries that modify the database schema.

  • Write transactions allow you to send query pipelines that may write data.

  • Read transactions allow you to send query pipelines that only read data.

How to open a transaction and run a query in it will depend on your client, but is very straight-forward in either case. Let’s open a simple schema transaction and then close it again, without actually running any queries.

Select your transaction type in the top bar: schema.

On the Query page, enter the following query:

define entity user;
typeql

Click Run query to run and commit your query.

TypeDB’s data model at a glance

Before we define a database schema for our newly created database, let’s have a quick look at TypeDB’s data model. In TypeDB, data is stored in types, which can be either:

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

  • relation types (e.g., all the friendship's of users)

  • attribute types (e.g., the username's of user's, or the start_date's of friendship's)

Stored data objects are called instances of types, and instances may reference other instances. This simple mechanism of “referencing” underpins the distinction of entity, relation, and attribute types above.

For example, to create a relation instance we must reference multiple other entities or relations, which we call the relation’s role players. Similarly, to create attribute instance we will reference a single entity or relation instances, which we call the attribute’s owner. Makes sense, right?

TypeDB’s data model is conceptually simple, but at the same time extremely systematic and flexible. This is what enables TypeDB’s type system to unify concepts from relational, document, and graph DBMSs, and blend them with a modern typed programming paradigm. Now, let’s write our first queries!

Note

A quick tip if you are using Console for this course. Queries on this page can be turned into runnable Console scripts by clicking the “hidden lines” button (eye). These scripts can then be pasted directly into the server-level interface of Console, and run end-to-end simply by pressing Enter.

Defining your type schema

Let’s define a simple database schema: we want to store user's and their username's in our database and, in addition, let’s also record friendship's between users. Types in our database schema are defined with define queries as follows.

On the Query page, select a schema transaction using the top bar as described above, then run these two queries.

define
  entity user, owns username;
  attribute username, value string;
typeql
define
  relation friendship, relates friend @card(2);
  user plays friendship:friend;
typeql

Let us dissect the second query in a bit more detail.

  • In the first line of the query we introduce the relation type friendship with a friend role (in programmer lingo: think of a role as a trait that other types can implement). In the same line, we also declare that each friendship requires exactly two friends when created (card is short for “cardinality”, and is a first example of an annotation. Annotations make schema statements much more specific - but more on that in a moment).

  • In the second line, we then define that users can play the role of friends in friendships (in programmer lingo: the type user implements the “friend-role” trait).

It’s worth pointing out that roles are only one of two kinds of traits that types may implement, the other being ownership: for example, in our first query above we defined that the user type “implements the username owner trait”. These traits make our life very easy as we’ll see, especially when dealing with type hierarchies!

Let’s write a few more definition queries to get a feel of database schemas in TypeDB.

First, note that relation types, like entity types, are types of “first-class objects” in our type system. In particular, they too can own attributes (and may play roles in other relation types). As an example, let’s run the following query:

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

This defines a start-date attribute and then declares each friendship may own a start_date (i.e., the type friendship implements the “start-date owner” trait). The definition is followed by a @card annotation, which specifies that friendships own between zero or one start dates. In fact, this is the default cardinality, so you don’t actually need @card here at all.

The next query 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
  attribute status, value string;
  user owns status @card(0..);
  user owns username @key;
typeql

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. Run the next query to define an organization type which shares many of the traits of user:

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

Note: By default, Studio auto-commits all schema and write queries. You may not always want this! For more fine-grained transaction control, you can change the mode to manual in the top bar.

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. The next example illustrates how this works.

Let’s define two types that specialize our earlier organization type: namely, we’ll define company and university as special cases, i.e., subtypes of organization. The keyword to define subtypings is sub. Run the following query:

define
  entity company sub organization;
  entity university sub organization;
typeql

Note that we haven’t defined any traits for companies and universities; indeed, all traits of the supertype organization are automatically inherited! That doesn’t stop from defining new traits for our subtypes, of course:

define
  attribute student-count, value integer;
  relation enrolment, relates university, relates student;
  university owns student-count, plays enrolment:university;
  user plays enrolment:student;
typeql

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 roles and ownerships 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 more details.

CRUD operations

Having defined the types in our schema, we are ready to write data to our database. Let’s see a few example of how we can create data. We will also learn how TypeDB’s type system holds us accountable when adding and modifying data, requiring us to conform to the database schema that we defined in the previous section.

To begin, set your transaction type to write using the top bar, then let us try and insert a user with the following insert query:

insert $x isa user;
typeql

The commit *will fail*! Indeed, we are trying to insert a user without a username here, but we declared usernames to be a key attribute earlier when we defined user owns username @key. (In fact, even if the insert query above worked, it would be pretty useless: we are trying to insert an entity with type user, but we give no information about that entity and thus no way to refer to the entity. So our schema keeps us accountable in this case.)

The following is more meaningful:

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

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";  # can split `isa` and `has` across different statements
typeql

This query inserts two users at the same time.

Finally, since we set usernames to be a @key attribute of our users, we would - rightfully - expect the following query to fail:

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

And it does fail: this time, it already fails at runtime, i.e., even before committing the transaction. Indeed, because the key "user_3" would be claimed by two distinct user instances no matter what, we can immediately invalidate the transaction.

This illustrates how we can insert simple entities with attributes, and how TypeDB keeps us accountable to follow the definitions in the database schema.

So far so good. Next question: what about inserting relations? Well, let’s have a go!

In order to insert a friendship, we need to refer to users that will play the role of friends in the friendship (recall: each friendship takes exactly two friends as specified in the schema; feel free to experiment though, and see what error messages TypeDB will give you when trying to insert triple friendships). That’s where variables come into play! Let’s run the following query:

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

This query inserts two new users with a friendship relation between them. The last line is a shorthand, and could itself be written as $f isa friendship (friend: $x, friend: $y); if you’d want to further use the variable $f; but otherwise it’s convenient to simply omit it.

But what if we want to create a friendship relation between two existing users already in the database? That requires a two-stage query pipeline - (1) retrieve the user with a match stage; (2) insert the new friendship. We will discuss pipelines in more detail below; for now let’s just run one:

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);
typeql

This query first matches three existing users, and inserts three new friendships between them.

Feel free to insert more data, e.g., create a university and enrol some of your users in it using our enrolment relations!

TL;DR Inserting data is easy!

And deleting and updating data works in a similar vein, with one additional detail: deleting and updating requires us to first match the data that we want to delete or update, whereas for inserts we saw that this was optional. This brings us to the topic of data pipelines.

Data pipelines in 100 seconds

Just like in functional programming, where you can “map” and “operate” on an iterator of data and chain these operations step-by-step, pipelines in TypeDB allow you to step-wise (or, rather, “stage-wise”) compose database operations. For example, you could:

  • First read some data from the database using a match stage;

  • Then feed the data you are reading into an insert operation that creates additional data;

  • Now, the insert operation (similar to other write operations like update and delete) itself returns data and this output data can be fed into yet another stage. For example, we could add a further match stage that retrieves further connected data and that we may want to operate on in a “next step”. This is illustrated in the example below.

We can continue chaining pipeline stages in this way, with the output data of one stage becoming the input data of the next stage. But let’s see some code first.

The following pipeline adds "VIP" status for a specific user, then traverses all friendships of that user, and marks friends as VIPs themselves if they now have more than 3 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 > 3;
insert $friend has status "VIP";
typeql

Notice how the variable $user is re-used throughout the first three stages of the pipeline. Also note how match stages are used both to retrieve new data and to filter data; both of these operations neatly fall into TypeQL’s declarative pattern language.

Running the above pipeline will return 0 answers if no friends of "user_0" have more than 3 “VIP” status friends themselves. Nonetheless, the pipeline still did some work. Let’s verify this:

match
  $user isa user, has status "VIP", has username $username;
select $username;
typeql

The query first matches users with “VIP” status and their usernames, and then selects only the username. Here, select is “stream manipulation” stage (meaning it manipulates the data stream in a pipeline without calling back to the databases). In the case of select, this manipulation simply removes all non-selected variables from our results. (Try deleting the last line and comparing answers produced by the database.) The reduce stage in the previous example works similarly!

Note

All read and write stages (match, select, insert, delete, update, …​) are can be pipelined; with the only exception being the fetch stage as this returns JSON documents (and not “concept rows”, TypeDB’s internal answer format). In contrast, schema queries (like define, undefine, redefine) do not return answers, and thus cannot be used in pipelines.

For a full rundown of pipeline stages, see the CRUD manual!

“Query programming” with TypeDB

We’ve seen how to define detailed database schemas, and how to insert connected data into the database. Now let’s turn to the simplest, but most ubiquitous database operation: reading data!

Reading data in an intuitive and composable programmatic way is TypeDB’s superpower, and achieved through its query language TypeQL which combines declarative and functional programming concepts with the simplicity of natural language. Let’s start with a basic example:

Use the top bar transaction control panel to set your transaction type to read.

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

The query in the example 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.

Note

The pattern $var isa <type> means the variable $var represents a data object (instance) of type <type>.

In addition, TypeQL has special statements to capture references between instances (as relations must references role players, and attributes must reference owners), like $relation isa <type> (<role>: $player); or $owner has <type> $attribute.

The above query is mildly interesting. But let’s look at a slightly more complex version of the same query.

Run the following read query in Studio and see what answers you get.

# Find users with more friends that Alex
match
  $alex isa user, has username "grothendieck_25";
  friendship (friend: $alex, friend: $friend-of-alex);
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;
typeql

Can you see just from reading the query what’s going on here?

The goal of this second 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.)

# Find users with more friends that Alex
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;
typeql

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. In fact, function can contain entire read pipelines themselves! This is a key component of TypeDB’s query programming paradigm, allowing you to seamlessly fuse "query modules" together.

What next?

There is much more to explore in the world of TypeDB, including building custom query logic using disjunctions and negations, the deep exploration of data connection using functional recursion, result formatting features, native language integration features, and more. This is the beginning of your journey, and we’d like to be here for you should you have any question - so do not hesitate to reach out!

And here are some suggestion what to look at next …​ (:

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.