Fauna for DynamoDB users

This guide outlines major differences between Amazon DynamoDB and Fauna. The guide also translates common DynamoDB queries to Fauna Query Language (FQL) and Fauna Schema Language (FSL).

Major differences

The following table outlines major differences between DynamoDB and Fauna.

Difference DynamoDB Fauna

Data model

DynamoDB is a key-value datastore. Data is stored as simple key-value pairs or as documents.

Combines document flexibility with native relational capabilities. Stores data as denormalized and normalized JSON-like documents in collections with relationship traversals.

Query capabilities

Does not support joins, subqueries, aggregations, or complex filtering.

Supports joins, subqueries, aggregations, and complex filtering through FQL.

Indexes

Requires a primary key that can not be changed after creation. Secondary indexes are limited to a maximum of 20 per table.

Global secondary indexes (GSIs) are eventually consistent. Local secondary indexes (LSIs) can limit table performance.

There is no primary key constraint. Indexes are strongly consistent. Fauna provides native support for secondary indexes without limitations found in DynamoDB, such as the ability to index nested fields and Arrays. Fauna does not limit the number of indexes per collection.

Consistency

Eventual consistency across regions with no guarantee of data consistency at any given time. Reads may not show the latest write.

Strong consistency by default across multiple regions, continents, and cloud providers. Fauna guarantees that all reads show the latest write, even across regions.

Distribution

Single region. Can be configured for multi-region with global tables.

Multi-region by default.

Schema

Performance heavily depends on data distribution and common access patterns.

Good performance requires a well designed schema and a deep understanding of application access patterns from the start.

Adapting or evolving a schema to new access patterns can requires significant work.

Flexible schema model that allows you to adapt to new or evolving access patterns with built-in tools for zero-downtime schema migrations. FSL enables version control for schema changes, integration with CI/CD pipelines, and progressive schema enforcement as applications evolve.

Operability

Simple Language based on the PUT and GET HTTP methods. Logic must live in application code. Doesn’t allow you to read data and use that data in a write operation in the same transaction.

Fauna uses FQL, a declarative Language that allows you to express complex data logic in a single query. FQL supports user-defined functions (UDF) to encapsulate logic and reuse it across queries. Allows for multi-line read, write, update operations in the same transaction. You can use reads as part of the writes/updates while keeping the transaction consistent.

Concepts

The following table maps common concepts from DynamoDB to equivalent in Fauna.

DynamoDB Fauna Notes

Database

Table

Partition Key (Simple Primary Key)

You can mimic some aspects of a primary key using a unique constraint and an index term. See Create a table.

Partition Key and Sort Key (Composite Primary Key)

Flexible indexing system

You can create multiple indexes on a collection, allowing for various query patterns without being constrained to a single primary key structure.

Index 

Fauna indexes must be named. This encourages better readability and more predictable behavior.

Examples

The following examples compare basic operations in DynamoDB and Fauna. DynamoDB examples use the AWS CLI. Fauna examples use FQL and FSL.

Create and manage tables or collections

Create a table

Create a table named Product with a primary key id.

aws dynamodb create-table \
    --table-name Product \
    --attribute-definitions AttributeName=id,AttributeType=S \
    --key-schema AttributeName=id,KeyType=HASH \
    --provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5 \
    --region us-west-2

To create a collection, create a collection schema in the database.

An example FSL collection schema:

// Defines the `Product` collection.
collection Product {
  // Wildcard constraint.
  // Allows arbitrary ad hoc fields of any type.
  *: Any
  // If a collection schema has no field definitions
  // and no wildcard constraint, it has an implicit
  // wildcard constraint of `*: Any`.
}

You can create and manage schema using any of the following:

List all tables

aws dynamodb list-tables
Collection.all()

Delete a table

aws dynamodb delete-table --table-name Customer

To delete a collection, delete its schema using the Dashboard or Fauna CLI. Deleting a collection deletes its documents and indexes.

Create and manage documents

Create a document

aws dynamodb put-item \
    --table-name Customer \
    --item '{
        "name": {"S": "John Doe"},
        "email": {"S": "jdoe@example.com"},
        "address": {
            "M": {
                "street": {"S": "87856 Mendota Court"},
                "city": {"S": "Washington"},
                "state": {"S": "DC"},
                "postalCode": {"S": "20220"},
                "country": {"S": "US"}
            }
        }
    }'
Customer.create({
  name: "John Doe",
  email: "jdoe@example.com",
  address: {
    street: "87856 Mendota Court",
    city: "Washington",
    state: "DC",
    postalCode: "20220",
    country: "US"
  }
})

Edit a document

The following example updates the name attribute of a document. In this example, the email attribute is the primary key.

aws dynamodb update-item \
    --table-name Customer \
    --key '{
        "email": {"S": "jdoe@example.com"}
    }' \
    --update-expression "SET #nm = :name" \
    --expression-attribute-names '{
        "#nm": "name"
    }' \
    --expression-attribute-values '{
        ":name": {"S": "Jane Doe"}
    }'

The following example gets the first document with a specific email address and updates its name field value.

Customer.firstWhere(.email == "jdoe@example.com")?.update({
  name: "Jane Doe"
})

For better performance on large datasets, use an index with the email term to run an exact match search.

Define the index in the collection schema:

collection Customer {
  ...

  // Defines the `byEmail()` index for the `Customer`
  // collection.
  index byEmail {
    // Includes the `email` field as an index term.
    terms [.email]
    values [.name]
  }
}

You call an index as a method on its collection:

// Uses the `Customer` collection's `byEmail()` index
// to run an exact match search on an `email` field value.
Customer.byEmail("jdoe@example.com").first()?.update({
  name: "Jane Doe"
})

Update nested document fields

aws dynamodb update-item \
    --table-name Customer \
    --key '{
        "email": {"S": "jdoe@example.com"}
    }' \
    --update-expression "SET address.#city = :city" \
    --expression-attribute-names '{
        "#city": "city"
    }' \
    --expression-attribute-values '{
        ":city": {"S": "Newtown"}
    }'
Customer.byEmail("jdoe@example.com").first()?.update({
  address: {
    street: "87856 Mendota Court",
    city: "Newtown",
    state: "DC",
    postalCode: "20220",
    country: "US"
  }
})

Delete a document

aws dynamodb delete-item \
    --table-name Customer \
    --key '{
        "email": {"S": "jdoe@example.com"}
    }'
Customer.byEmail("jdoe@example.com").first()?.delete()

Perform bulk writes

The following examples set the status field to gold for existing Customer collection documents.

First, retrieve the existing items from the Customer table.

aws dynamodb scan --table-name Customer --attributes-to-get "email" > customers.json

Next, prepare a JSON file named batch-write.json with the items you want to update.

{
  "Customer": [
    {
      "PutRequest": {
        "Item": {
          "email": { "S": "jdoe@example.com" },
          "address": {
            "M": {
              "state": { "S": "District of Columbia" }
            }
          }
        }
      }
    },
    {
      "PutRequest": {
        "Item": {
          "email": { "S": "bob.brown@example.com" },
          "address": {
            "M": {
              "state": { "S": "District of Columbia" }
            }
          }
        }
      }
    },
    {
      "PutRequest": {
        "Item": {
          "email": { "S": "carol.clark@example.com" },
          "address": {
            "M": {
              "state": { "S": "District of Columbia" }
            }
          }
        }
      }
    }
  ]
}
aws dynamodb batch-write-item --request-items file://batch-write.json
// Get a Set of `Customer` collection documents with an
// `address` in the `state` of `DC`.
let customers = Customer.where(.address?.state == "DC")
// Use `forEach()` to update each document in the previous Set.
customers.forEach(doc => doc.update({
  address: {
    street: doc?.address?.street,
    city: doc?.address?.city,
    state: "District of Columbia",
    postalCode: doc?.address?.postalCode,
    country: doc?.address?.country,
  }
})) // `forEach()` returns `null`.

Indexes and read queries

Create an index

Run the following command to create a GSI named byEmail() on the Customer table.

aws dynamodb update-table \
    --table-name Customer \
    --attribute-definitions AttributeName=Email,AttributeType=S \
    --global-secondary-index-updates \
        "[{\"Create\":{\"IndexName\": \"byEmail\",\"KeySchema\":[{\"AttributeName\":\"Email\",\"KeyType\":\"HASH\"}],\"Projection\":{\"ProjectionType\":\"ALL\"},\"ProvisionedThroughput\":{\"ReadCapacityUnits\":5,\"WriteCapacityUnits\":5}}}]"

If using global tables, you must run this separately in each region. GSIs don’t automatically replicate across regions.

DynamoDB can only do this on top-level attributes. If there is a nested attribute, it cannot index those.

In Fauna, you define and manages indexes in FSL as part of a collection schema:

collection Customer {
  ...

  index byEmail {
    // `terms` are document fields for exact match searches.
    // In this example, you get `Customer` collection documents
    // by their `email` field value.
    terms [.email]
    // `values` are document fields for sorting and range searches.
    // In this example, you sort or filter index results by their
    // descending `firstName` and `lastName` field values.
    values [.firstName, .lastName]
  }
}

Fauna indexes are globally replicated automatically across all regions. You don’t need to create indexes in each region. You can also index nested fields.

aws dynamodb query \
    --table-name Customer \
    --index-name byEmail \
    --key-condition-expression "Email = :email" \
    --expression-attribute-values '{":email":{"S":"user@example.com"}}'
// Runs an unindexed query.
Customer.where(.email == "jdoe@example.com")

For better performance on large datasets, use an index with the email term to run an exact match search.

You call an index as a method on its collection:

// Uses the `Customer` collection's `byEmail()` index
// to run an exact match search on an `email` field value.
Customer.byEmail("jdoe@example.com")

Sort collection documents

Find all products that has a name of pizza and sort them by ascending price.

Make sure your DynamoDB table has a partition key name and sort key price.

aws dynamodb create-table \
    --table-name Product \
    --attribute-definitions \
        AttributeName=name,AttributeType=S \
        AttributeName=price,AttributeType=N \
    --key-schema \
        AttributeName=name,KeyType=HASH \
        AttributeName=price,KeyType=RANGE \
    --provisioned-throughput \
        ReadCapacityUnits=5,WriteCapacityUnits=5
aws dynamodb query \
    --table-name Product \
    --key-condition-expression "#name = :name" \
    --expression-attribute-names '{"#name": "name"}' \
    --expression-attribute-values '{":name": {"S": "pizza"}}' \
    --no-scan-index-forward \
    --output json
// Runs an unindexed query.
// Returns a Set of `Product` collection documents with
// a `name` of `pizza`. Sorts the documents by:
// - `price` (ascending)
Product.where(.name == 'pizza').order(.price)

For better performance on large datasets, use an index with values to sort collection documents.

collection Product {
  ...

  // Defines the `byName()` index for the `Product` collection.
  index byName {
    // Includes the `name` field as an index term.
    // Use the term to run exact match searches.
    terms [.name]
    // Includes `price` as an index value.
    // Sorts the documents by:
    // - `price` (ascending)
    values [.price]
  }
}

Call the index in a query:

// Gets a Set of products with a `name` of `pizza`.
// The Set is sorted by `price` (ascending).
Product.byName("pizza")

Is this article helpful? 

Tell Fauna how the article can be improved:
Visit Fauna's forums or email docs@fauna.com

Thank you for your feedback!