Skip to content

DynamoDB

Imaginemos una aplicacion de Propietarios que tienen pisos y los alquilan a inquilinos. Un Propietario puede tener varios pisos.

Conceptos basicos para diseñar una table

Al igual que en kafka, para escalar horizontalmente Dynamo guarda los elementos en base a su clave de particion.

Partition KEY

Buscador basico usando PropietarioId

En un sistema de jerarquia tradicional de bbdd relacional, buscariamos al propietario y de ahi buscariamos los pisos que le corresponden. Es decir creamos un registro con PK PropietarioID y buscamos por el, cuando obtenemos el body leemos los pisos y buscamos.

Desventajas: Tendriamos una query para el propietario y N querys por cada piso.

Partition KEY + Global secondary index

Arrancando DynamoDB

docker run -p 8000:8000 amazon/dynamodb-local

Creamos la tabla en la dynamo local

aws dynamodb create-table \
--table-name Shops \
--billing-mode PAY_PER_REQUEST  \
--attribute-definitions \
AttributeName=ShopId,AttributeType=S  \
AttributeName=OwnerId,AttributeType=S \
AttributeName=CreatedAt,AttributeType=S \
--key-schema \
AttributeName=ShopId,KeyType=HASH \
--global-secondary-indexes '[
{
"IndexName": "OwnerIdIndex",
"KeySchema": [
    {"AttributeName": "OwnerId", "KeyType": "HASH"},
    {"AttributeName": "CreatedAt", "KeyType": "RANGE"}
],
"Projection": {"ProjectionType": "ALL"}
}
]' \
--endpoint-url http://localhost:8000
Verificamos que la tabla se ha creado

  aws dynamodb describe-table --table-name Shops --endpoint-url http://localhost:8000

Creamos la primera tienda (La tienda no puede existir si no hay propietarios, por eso la clave nos obliga a apuntarlo.)

aws dynamodb put-item --table-name Shops \
--item '{                                                                                                                                        
    "ShopId":    {"S": "shop-001"},
    "OwnerId":   {"S": "owner-123"},                                                                                                               
    "CreatedAt": {"S": "2024-03-15T10:30:00Z"},
    "Name":      {"S": "My Coffee Shop"},
    "Address":   {"S": "123 Main St"},
    "Active":    {"BOOL": true}
}' \
--endpoint-url http://localhost:8000


DynamoDB type descriptors

┌────────────┬─────────────────────┬─────────────────────────────────┐ │ Descriptor │ Type │ Example │ ├────────────┼─────────────────────┼─────────────────────────────────┤ │ S │ String │ {“S”: “hello”} │ ├────────────┼─────────────────────┼─────────────────────────────────┤ │ N │ Number │ {“N”: “42”} │ ├────────────┼─────────────────────┼─────────────────────────────────┤ │ BOOL │ Boolean │ {“BOOL”: true} │ ├────────────┼─────────────────────┼─────────────────────────────────┤ │ NULL │ Null │ {“NULL”: true} │ ├────────────┼─────────────────────┼─────────────────────────────────┤ │ L │ List │ {“L”: [{“S”: “a”}, {“N”: “1”}]} │ ├────────────┼─────────────────────┼─────────────────────────────────┤ │ M │ Map (nested object) │ {“M”: {“key”: {“S”: “val”}}} │ ├────────────┼─────────────────────┼─────────────────────────────────┤ │ SS │ String Set │ {“SS”: [“a”, “b”]} │ ├────────────┼─────────────────────┼─────────────────────────────────┤ │ NS │ Number Set │ {“NS”: [“1”, “2”]} │ └────────────┴─────────────────────┴─────────────────────────────────┘


La busqueda basica por PK seria por shopId

aws dynamodb get-item \
--table-name Shops \
--key '{"ShopId": {"S": "shop-001"}}' \
--endpoint-url http://localhost:8000

Creamos un par de tiendas mas

aws dynamodb put-item --table-name Shops \
--item '{                                                                                                                                        
    "ShopId":    {"S": "shop-002"},
    "OwnerId":   {"S": "owner-123"},                                                                                                               
    "CreatedAt": {"S": "2024-03-15T10:30:00Z"},
    "Name":      {"S": "My Cool Shop"},
    "Address":   {"S": "000 Main St"},
    "Active":    {"BOOL": true}
}' \
--endpoint-url http://localhost:8000
aws dynamodb put-item --table-name Shops \
--item '{                                                                                                                                        
    "ShopId":    {"S": "shop-003"},
    "OwnerId":   {"S": "owner-123"},                                                                                                               
    "CreatedAt": {"S": "2024-03-15T10:30:00Z"},
    "Name":      {"S": "My third shop"},
    "Address":   {"S": "003 Main St"},
    "Active":    {"BOOL": true}
}' \
--endpoint-url http://localhost:8000

Y creamos tiendas para u nuevo Owner 456

aws dynamodb put-item --table-name Shops \
--item '{                                                                                                                                        
    "ShopId":    {"S": "owner456-shop1"},
    "OwnerId":   {"S": "owner-456"},                                                                                                               
    "CreatedAt": {"S": "2024-03-15T10:30:00Z"},
    "Name":      {"S": "My Amazing shop"},
    "Address":   {"S": "0066 Main St"},
    "Active":    {"BOOL": true}
}' \
--endpoint-url http://localhost:8000

aws dynamodb put-item --table-name Shops \
--item '{                                                                                                                                        
    "ShopId":    {"S": "owner456-shop2"},
    "OwnerId":   {"S": "owner-456"},                                                                                                               
    "CreatedAt": {"S": "2024-03-15T10:30:00Z"},
    "Name":      {"S": "My Second Amazing shop"},
    "Address":   {"S": "0456066 Main St"},
    "Active":    {"BOOL": true}
}' \
--endpoint-url http://localhost:8000
aws dynamodb put-item --table-name Shops \
--item '{                                                                                                                                        
    "ShopId":    {"S": "owner456-shop3"},
    "OwnerId":   {"S": "owner-456"},                                                                                                               
    "CreatedAt": {"S": "2022-03-15T10:30:00Z"},
    "Name":      {"S": "My Legacy shop"},
    "Address":   {"S": "0456066 Main St"},
    "Active":    {"BOOL": true}
}' \
--endpoint-url http://localhost:8000

Busqueda Sencilla por Owner

Vamos a buscar por el Owner (es decir un listado de todas las tiendas del Owner 1) Usamos el parametro –index-name para decirle que busque por el GSI en vez del primary key (ShopId)

aws dynamodb query --table-name Shops \
--index-name OwnerIdIndex \
--key-condition-expression "OwnerId = :oid" \
--expression-attribute-values '{":oid": {"S": "owner-123"}}' \
--endpoint-url http://localhost:8000

Busqueda usando ordenacion SortKeys (–scan-index-forward)

Vamos a listar por Owner y que nos filtre por createdAd para ello usamos una de las 2 opciones:

--scan-index-forward 
--no-scan-index-forward
Esto funciona porque tenemos un atributo de tipo RANGE
{"AttributeName": "CreatedAt", "KeyType": "RANGE"}

Por defecto este parametro (–scan-index-forward) y nos devolvera los elementos siguiendo el orden natural del rango, asi que nos devolvera los que primero creamos antes. Con el parametro false hace las busqueda inversa en el rango (–no-scan-index-forward), es decir empieza por el final, es muy util por ejemplo para listados de citas, facturas, etc..

aws dynamodb query --table-name Shops --index-name OwnerIdIndex --scan-index-forward --key-condition-expression "OwnerId = :oid" --expression-attribute-values '{":oid": {"S": "owner-456"}}' --endpoint-url http://localhost:8000
aws dynamodb query --table-name Shops --index-name OwnerIdIndex --no-scan-index-forward --key-condition-expression "OwnerId = :oid" --expression-attribute-values '{":oid": {"S": "owner-456"}}' --endpoint-url http://localhost:8000

CONTROL AVANZANDO SOBRE EL RANGO

Los parametros RANGE pordemos usarlos en query avanzada

aws dynamodb query \
--table-name Shops \
--index-name OwnerIdIndex \
--key-condition-expression "OwnerId = :oid AND CreatedAt BETWEEN :from AND :to" \
--expression-attribute-values '{
    ":oid":  {"S": "owner-456"},
    ":from": {"S": "2024-01-01T00:00:00Z"},
    ":to":   {"S": "2024-12-31T23:59:59Z"}
}' \
--endpoint-url http://localhost:8000

aws dynamodb query \
--table-name Shops \
--index-name OwnerIdIndex \
--key-condition-expression "OwnerId = :oid AND CreatedAt BETWEEN :from AND :to" \
--expression-attribute-values '{
    ":oid":  {"S": "owner-456"},
    ":from": {"S": "2021-01-01T00:00:00Z"},
    ":to":   {"S": "2023-12-31T23:59:59Z"}
}' \
--endpoint-url http://localhost:8000

EXPERIMENTAL

Vamos a crear una tabla con 2 o mas GSI para ver si lo sporta

tabla Tenant, es lo mismo pero a parte del OwnerId como GSI vamos a poner tambien GSI country

aws dynamodb create-table --table-name ShopsGEO --billing-mode PAY_PER_REQUEST \
--attribute-definitions \
AttributeName=ShopId,AttributeType=S \
AttributeName=OwnerId,AttributeType=S \
AttributeName=CreatedAt,AttributeType=S \
AttributeName=GeohashPrefix,AttributeType=S \
AttributeName=Geohash,AttributeType=S \
--key-schema \
AttributeName=ShopId,KeyType=HASH \
--global-secondary-indexes '[
    {
    "IndexName": "OwnerIdIndex",
    "KeySchema": [
        {"AttributeName": "OwnerId", "KeyType": "HASH"},
        {"AttributeName": "CreatedAt", "KeyType": "RANGE"}
    ],
    "Projection": {"ProjectionType": "ALL"}
    },
    {
    "IndexName": "GeohashIndex",
    "KeySchema": [
        {"AttributeName": "GeohashPrefix", "KeyType": "HASH"},
        {"AttributeName": "Geohash", "KeyType": "RANGE"}
    ],
    "Projection": {"ProjectionType": "ALL"}
    }
]' \
--endpoint-url http://localhost:8000

CREAMOS LA PRIMERA TIENDA: Usamos la funcion geohash que hay varias libs para convertir latitude y longitude en geohash http://geohash.co/

Madrid en este caso tiene esta latitude longitude 40.4165, -3.70256 su GEOHASH es ezjmgtxg y su prefix por lo tanto lo cogemos con poca exactitud y nos valen las 5 primeras letras ezjmg

Guadarrama tiene estas 40.6817357, -4.0909462 GEOHASH: ezjp6vvh (vemos que el prefix es el mismo)

Inertamos la tienda en guadarrama

aws dynamodb put-item \
--table-name ShopsGEO \
--item '{
    "ShopId":        {"S": "shop-001"},
    "OwnerId":       {"S": "owner-123"},
    "CreatedAt":     {"S": "2024-03-15T10:30:00Z"},
    "Name":          {"S": "Guadarrama Shop"},
    "GeohashPrefix": {"S": "ezjp"},
    "Geohash":       {"S": "ezjp6vvh"}
}' \
--endpoint-url http://localhost:8000

Aluche tiene estas 40.3875, -3.7542 GEOHASH : ezjmf866

aws dynamodb put-item \
--table-name ShopsGEO \
--item '{
    "ShopId":        {"S": "shop-002"},
    "OwnerId":       {"S": "owner-123"},
    "CreatedAt":     {"S": "2024-03-15T10:30:00Z"},
    "Name":          {"S": "Aluche Shop"},
    "GeohashPrefix": {"S": "ezjm"},
    "Geohash":       {"S": "ezjmf866"}
}' \
--endpoint-url http://localhost:8000

BUSQUEDA GEO

La busqueda se produce en funcion de una localizacion dada y un radio

aws dynamodb query \
--table-name ShopsGEO \
--index-name GeohashIndex \
--key-condition-expression "GeohashPrefix = :prefix AND begins_with(Geohash, :geo)" \
--expression-attribute-values '{
    ":prefix": {"S": "gcpv"},
    ":geo":    {"S": "gcpvh6"}
}' \
--endpoint-url http://localhost:8000
begins_with on the sort key lets you progressively narrow the search radius — the longer the geohash prefix, the smaller the area:

┌────────────────┬────────┐ │ Geohash length │ Area │ ├────────────────┼────────┤ │ 4 chars │ ~40 km │ ├────────────────┼────────┤ │ 5 chars │ ~5 km │ ├────────────────┼────────┤ │ 6 chars │ ~1 km │ ├────────────────┼────────┤ │ 7 chars │ ~150 m │ ├────────────────┼────────┤ │ 8 chars │ ~40 m │

En este caso la query no devuleve resultados porque estoy usando
“:prefix”: {“S”: “gcpv”}, “:geo”: {“S”: “gcpvh6”}

Vamos a probar desde madrid capital 40.4165, -3.7026 geohash: ezjmgtxg

Reduciendo el hash consigo que me encuentre Aluche

aws dynamodb query \
--table-name ShopsGEO \
--index-name GeohashIndex \
--key-condition-expression "GeohashPrefix = :prefix AND begins_with(Geohash, :geo)" \
--expression-attribute-values '{
    ":prefix": {"S": "ezjm"},
    ":geo":    {"S": "ezj"}
}' \
--endpoint-url http://localhost:8000

Vamos a ver si coge las 2 quitando y poniendo el prefijo mas corto, ya que guadarrama me di cuenta de que tiene otro prefix “ezjp”, asi que vamos con menos prefix

aws dynamodb query \
--table-name ShopsGEO \
--index-name GeohashIndex \
--key-condition-expression "GeohashPrefix = :prefix AND begins_with(Geohash, :geo)" \
--expression-attribute-values '{
    ":prefix": {"S": "ezjp"},
    ":geo":    {"S": "ezjp"}
}' \
--endpoint-url http://localhost:8000

HE reducido pero no encontraba, he tenido que afinar el prefix, debe ser que con un prefix de 3 caracteres no busca ni lo intenta, es demasiado ambiguo