My experience with EdgeDB

Tomorrow is an announcement of EdgeDB 2.0, and I realized that I’ve never shared my experience with it, and it’s been more than a year since I switched every project I could to EdgeDB. So here is a short post on it.

What is EdgeDB?

It’s an open-source relational/graph database built on top of Postgres.

Unlike many other databases, it doesn’t reinvent the underlying engine and uses battle-tested Postgres. But it reinvents everything else for you as a database user. For me, the two most important innovations here are – query language and tooling.

Query language

There is no SQL in EdgeDB. It has its own query language called EdgeQL, and it’s a game changer.

It turns this:

SQL

into this:

EdgeQL

After EdgeQL, I don’t want to return to SQL ever again. It’s strongly typed, object-oriented, makes linking stuff a toy, enables stupidly easy deep querying, and maps to how humans think about querying data really well (humans don’t think in JOINs, for example).

I wasn’t a fan of SQL before, but let’s admit, there really was no alternative. We had to hone our SQL skills and become increasingly skilled in SQL.

The thing is, after a couple of days of learning and practicing EdgeQL (with a fantastic book-style tutorial), I realized that database schema modeling became an unusually pleasant and lightweight task. Once so much of the accidental complexity that comes with SQL suddenly evaporated, it became obvious how inefficient and weird SQL as a query language is.

After EdgeQL, I don’t want to return to SQL ever again.

So if you’re a hardcore SQL user proud of their 20+ years of SQL experience – don’t try EdgeQL. It’s always hard to downgrade your identity from “master in something overly-complicated” to “newbie in better-and-less-complicated”.

EdgeQL is actually easy to master. You can learn like 80% of it just from its quickstart and overview.

update

In fact, I like EdgeQL so much that I’d love it to be a standalone standard. It would be cool to have implementations of, say, EdgeDBLite - file-based database that talks EdgeQL. :)

Tooling

If you work with some technology daily, every bit of convenience or inconvenience sums up over time and forms your overall experience. I often use a satisfaction-money analogy - every minute spent using service/app/device is either +0.1$ or -0.1$ depending on how you feel using it. Well, EdgeDB experience translates to just earning money non-stop.

It follows the already popular pattern of one binary that does everything (think go or flutter commands). Install or update servers, create/destroy database, repl, manage migrations, backups and projects – all in one package, and most of the time, “it just works”. EdgeDB reinvents a bit the way how you interact with the database - the concept of “project” links the database to your project automagically, so you don’t need to bother with DSNs or configurations. It “just works”.

After decades of constant googling the exact SQL command for granting root access for localhost, that’s very refreshing.

Features

Some of the features you might find interesting:

My experience so far

I started using EdgeDB more than one year ago. First, I migrated a small personal project, reflected a bit, then switched one of the company’s projects. Most of my projects are relatively small practical user-facing services, and I often have the luxury of working alone on the whole thing and the freedom to choose tools I see as most fit. I wish I could tell more, but I’m migrating couple of public projects, so will blog about it once they up.

With EdgeDB, it’s mostly an “it just works” experience. Most things are easily discoverable through tinkering, but when you can’t figure out something – EdgeDB has fantastic documentation. Really, one of the best docs sites I’ve used.

Production readiness

I have used EdgeDB “in production” since last autumn. I use quotes because some people might put more constraints on the definition of “in production” than just being used in actual business service. Since then EdgeDB command line tool went through some refactoring, I had to update my automation scripts once (like replacing migrate with migration command name), but that’s it.

Mentally I was prepared to pay the price for risks associated with early-adopter tech, but there were zero issues so far.

Expressiveness

The time I used to spend on googling SQL wizardry sites or ORM limitations workarounds now freed. EdgeQL just makes sense. In most cases, I just express what exactly I want to get from DB, and it’s actually readable. Meaning, another developer will be able to understand what exactly I was asking from the database just by reading the query.

Also, often I can avoid transactions because with EdgeQL, I can do multiple nested updates with a single query. Simplicity is complicated, yeah.

Migrations

One of the coolest things is having migrations out of the box. You change the schema, run edgedb migration create, which generates a migration file, and on the server, apply it with edgedb migration apply. Migrations are chained with hashes, so you can’t just randomly change migration without screwing everything up :) Also, when you create new migrations, the default mode is interactive, and the command line prompt asks you to confirm every change. While I know it’s relatively easy to code, it feels “smart” - like some careful assistant goes through your changes and asks you to double-check - did you really make this property required?.

Performance

I don’t have a high demand for database performance, as I don’t work with petabytes of data and try to avoid high-load as much as I can, but having Postgres under the hood suggests that speed is not going to be a bottleneck (compared to other databases). EdgeDB itself is written in Python (I wish it was in Go, but authors were more skilled and confident in Python), but, again, it’s mostly wrapper/edgeql/cli/pool layer, not the actual query engine.

There were some benchmarks around. Probably this one is the best link to start: https://github.com/edgedb/imdbench

performance

Go client library

I use Go for all backend stuff, so having native Go library for EdgeQL was a pleasant discovery at the beginning. EdgeDB has official libraries for Typescript/JS, Python, and Go, and community ones for .Net and Elixir.

Go library is not as developed as Typescript’s one, but it’s quite complete and actively maintained. It obviously doesn’t use stdlib’s database/sql because who needs SQL anymore.

package main

import (
    "context"
    "log"

    "github.com/edgedb/edgedb-go"
)

func main() {
    ctx := context.Background()
    client, err := edgedb.CreateClient(ctx, edgedb.Options{})
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    var (
        age   int64 = 21
        users []struct {
            ID   edgedb.UUID `edgedb:"id"`
            Name string      `edgedb:"name"`
        }
    )

    query := "SELECT User{name} FILTER .age = <int64>$0"
    err = client.Query(ctx, query, &users, age)
    ...
}

As you can see, it’s relatively low-level and expressive. If you have projects where the database layer is adequately isolated, rewriting client code from, say, MySQL to EdgeDB is fairly straightforward (in terms of using the library, not the queries).

My only trouble with Go implementation is the implementation of optional values. EdgeQL has a concept of optional, which is similar to NULL in SQL. In many Go database drivers, we used to have sql.Null<Type> and EdgeDB Go library introduced edgedb.Optional<Type>. While I agree that this approach is expressive and correct, I often find myself wishing it was just using pointers for optional values. Switching field from required to optional sometimes means rewriting too many lines of code just to allow that, and it often looks ugly.

// Club is used to read Club from EdgeDB.
type Club struct {
	ID     edgedb.UUID `edgedb:"id"`
	Name   string      `edgedb:"name"`
	City   City        `edgedb:"city"`

	Members []Person `edgedb:"members"`

	edgedb.Optional
}

// Get, Set and Unser implement OptionalMarshaller for edgedb.
func (b *Club) Get() (Club, bool) { return *b, !b.Missing() }
func (b *Club) Set(club Club)     { *b = club; b.SetMissing(false) }
func (b *Club) Unset()            { b.SetMissing(true) }

For example, in the code that converts data model structure to protobuf, now optional City field requires 5 more lines instead of 1. I have most of this code autogenerated, but still – it takes time to figure out how to deal with optionals in EdgeDB Go library.

// clubToPB converts DB Club to the protobuf structure.
func clubToPB(club *db.Club) *pb.Club {
	ret := &pb.Club{
		Id:     club.ID.String(),
		Name:   club.Name,
		// City:   cityToPB(club.City),
	}

	// optional fields
	if val, set := club.City.Get(); set {
		ret.City = cityToPB(&val)
	}

	return ret
}

Upgrades

EdgeDB abstracts away a lot of complexity related to the server versioning and updates. EdgeDB CLI tool basically handles everything itself. It shows you when new updates are available, shows installed server versions, allows you to upgrade instances, etc.

It works so smoothly it’s even suspicious.

But right now it pulls the new 2.0 RC server version, and I’m not worrying at all. All experience with EdgeDB tells me that it’s safe and easy.

upgrade

Communication and support

As an early adopter, I was and still am enjoying an extremely fast responses and an incredible level of help in the official EdgeDB channels. Sometimes I had situations where I could not find the best or most logical solution, and in most cases, there was a clear solution I overlooked in the docs.

I’m unsure if the same level of communication will be when 90% of the planet switches to EdgeQL, and kids will be asking “what is SQL?” but let’s see.

EdgeDB 2.0

So tomorrow’s release brings some new juicy stuff:

  • EdgeDB UI - web app with visual schema layout, editor, repl, and god knows what else. As anticipated, it looks fantastic, but I have yet to try it in practice.

UI

  • New stuff like group query, global schema variables, object-level security, new deletion policies (and long-awaited “on delete delete target”), direct EdgeQL over HTTP, and others.

Things I don’t like or worried about

I have two major concerns with EdgeDB and using it as a go-to DB for all the projects.

1. The promise of compatibility?

So far, everything is going great. Some minor updates, nothing with breaking changes. But I’d love to see a commitment to the promise of compatibility. EdgeDB seems to be well designed, so as soon as the design is good enough, there is an enormous value in making it permanent (at least for the next 10 years). It hurts to see how quickly community tutorials and bits of code became obsolete in a year or two because of incompatible API changes.

2. Growing Feature Set

EdgeDB already has way more features than I need, and the set seems to be growing. The more powerful EdgeQL becomes, the more it blurs the line between database and server, and the more choices I, as a developer, have to make - “should this be in a backend logic or this super-smart database schema?”. Those are day-to-day practical questions, and they keep adding cognitive load.

With an embedded HTTP server, it makes sense to push EdgeDB in the direction where it can replace the backend server completely for most projects. Still, I’d love to have just a good, reliable database that has this awesome query language and engine.

I tried to use inheritance, for example, but it doesn’t really map onto the Go types, so I had to model schema accordingly. Polymorphic fields are something I’ll probably never use. Or another example, in EdgeDB 2.0, there is a new cal::date_duration type, but is it justified? cal::local_date and cal::relative_duration are both super helpful types, but duration in days can be easily expressed in integers. Why another type? To simplify +/- operators in EdgeQL? Why not add meters_distance then? At some point, we’ll start to have libraries inside EdgeQL?

I know I’m a minority here – most people welcome more features – but I see a feature set as a vector space, with features being basis vectors. Every new feature increases the dimensionality and complexity of the optimal solution search within the feature set, which increases cognitive load. I’m not saying that finding the right balance for the feature set is easy, but it is worth the effort in the long run..

Anyway, let’s hope EdgeDB will be stable and consistent and won’t grow too much. So far, its feature set is just right.

Conclusion

In short, I don’t even consider conventional relational databases for future projects anymore. Switching back to SQL feels like changing from Flutter to Ncurses or Go to Assembler.

For me, EdgeDB is the single most important advance in databases in the last 20 years. I’m aware that I can change my mind in the future as I gain more experience, but hey, I’ve been using EdgeDB daily for more than a year, and so far, nothing has affected my excitement. The more I use it, the more I trust EdgeDB my data.

The work EdgeDB folks have done - with EdgeDB itself, query language, the website, tools, and community-building - is outright impressive. Mad respect to the team.

And it seems they are just warming up.

https://edgedb.com

EdgeDB 2.0 Launch day link is here, if you wonder: https://lu.ma/edgedb