Collections
When deciding on data structures it is important to understand their tradeoffs. Choosing the wrong structure can create a bottleneck as the application scales, and migrating the state to the new data structures will come at a cost.
You can choose between two types of collections:
- Native collections (e.g.
Array
,Map
,Set
), provided by the language - SDK collections (e.g.
IterableMap
,Vector
), provided by the NEAR SDK
Understanding how the contract stores and loads both types of collections is crucial to decide which one to use.
Use native collections for small amounts of data that need to be accessed altogether, and SDK collections for large amounts of data that do not need to be accessed altogether.
If your collection has up to 100 entries, it's acceptable to use the native collection. For larger ones, prefer to use SDK collection. For comparison please refer to this benchmark.
How the State is Handled
Each time the contract is executed, the first thing it will do is to read the values and deserialize them into memory, and after the function finishes, it will serialize and write the values back to the database.
For native collections, the contract will fully load the collection into memory before any method executes. This happens even if the method you invoke does not use the collection. Know that this will have impact on GAS you spend for methods in your contract.
Native Collectionsβ
Native collections are those provided by the language:
- JS:
Array
,Set
,Map
,Object
... - Rust:
Vector
,HashMap
,Set
...
All entries in a native collection are serialized into a single value and stored together into the state. This means that every time a function execute, the SDK will read and deserialize all entries in the native collection.
Serialization & Storage Example
The array [1,2,3,4]
will be serialized into the JSON string "[1,2,3,4]"
in Javascript, and the Borsh byte-stream [0,0,0,4,1,2,3,4]
in Rust before being stored
Native collections are useful if you are planning to store smalls amounts of data that need to be accessed all together
As the native collection grows, deserializing it from memory will cost more and more gas. If the collections grows too large, your contract might expend all the gas trying to read its state, making it fail on each function call
SDK Collectionsβ
The NEAR SDKs expose collections that are optimized for random access of large amounts of data. SDK collections are instantiated using a "prefix", which is used as an index to split the data into chunks. This way, SDK collections can defer reading and writing to the store until needed.
These collections are built to have an interface similar to native collections.
Serialization & Storage Example
The sdk array [1,2,3,4]
with prefix "p"
will be stored as the string "p"
in the contract's attribute, and create four entries in the contract's storage: p-0:1
, p-1:2
...
SDK collections are useful when you are planning to store large amounts of data that do not need to be accessed all together
Exposed Collectionsβ
- π JavaScript
- π¦ Rust
- π¦ Rust (legacy)
- π Python
SDK Collection | Native Equivalent | Description |
---|---|---|
Vector | Array | A growable array type. The values are sharded in memory and can be used for iterable and indexable values that are dynamically sized. |
LookupSet | Set | A set, which is similar to LookupMap but without storing values, can be used for checking the unique existence of values. This structure is not iterable and can only be used for lookups. |
UnorderedSet | Set | An iterable equivalent of LookupSet which stores additional metadata for the elements contained in the set. |
LookupMap | Map | This structure behaves as a thin wrapper around the key-value storage available to contracts. This structure does not contain any metadata about the elements in the map, so it is not iterable. |
UnorderedMap | Map | Similar to LookupMap , except that it stores additional data to be able to iterate through elements in the data structure. |
SDK collection | std Β equivalent | Description |
---|---|---|
store::Vector<T> | Vec<T> | A growable array type. The values are sharded in memory and can be used for iterable and indexable values that are dynamically sized. |
store::LookupMap | HashMap | This structure behaves as a thin wrapper around the key-value storage available to contracts. This structure does not contain any metadata about the elements in the map, so it is not iterable. |
store::IterableMap | HashMap | Similar to LookupMap , except that it stores additional data to be able to iterate through elements in the data structure. |
store::UnorderedMap | HashMap | Similar to LookupMap , except that it stores additional data to be able to iterate through elements in the data structure. |
store::LookupSet<T> | HashSet<T> | A set, which is similar to LookupMap but without storing values, can be used for checking the unique existence of values. This structure is not iterable and can only be used for lookups. |
store::IterableSet<T> | HashSet<T> | An iterable equivalent of LookupSet which stores additional metadata for the elements contained in the set. |
store::UnorderedSet<T> | HashSet<T> | An iterable equivalent of LookupSet which stores additional metadata for the elements contained in the set. |
The near_sdk::collections
is now deprecated in favor of near_sdk::store
. To use near_sdk::collections
you will have to use the legacy
feature.
SDK collection | std Β equivalent | Description |
---|---|---|
collections::Vector<T> | Vec<T> | A growable array type. The values are sharded in memory and can be used for iterable and indexable values that are dynamically sized. |
collections::LookupMap | HashMap | This structure behaves as a thin wrapper around the key-value storage available to contracts. This structure does not contairn any metadata about the elements in the map, so it is not iterable. |
collections::UnorderedMap | HashMap | Similar to LookupMap , except that it stores additional data to be able to iterate through elements in the data structure. |
collections::TreeMap | BTreeMap | An ordered equivalent of UnorderedMap . The underlying implementation is based on an AVL tree. This structure should be used when a consistent order is needed or accessing the min/max keys is needed. |
collections::LookupSet<T> | HashSet<T> | A set, which is similar to LookupMap but without storing values, can be used for checking the unique existence of values. This structure is not iterable and can only be used for lookups. |
collections::UnorderedSet<T> | HashSet<T> | An iterable equivalent of LookupSet which stores additional metadata for the elements contained in the set. |
collections::LazyOption<T> | Option<T> | Optional value in storage. This value will only be read from storage when interacted with. This value will be Some<T> when the value is saved in storage, and None if the value at the prefix does not exist. |
SDK Collection | Native Equivalent | Description |
---|---|---|
Vector | list | A growable array type. The values are sharded in memory and can be used for iterable and indexable values that are dynamically sized. |
LookupMap | dict | A non-iterable key-value store. This structure does not track keys for iteration, so it is optimized for lookups but cannot provide collection operations like keys or values. |
UnorderedMap | dict | Similar to LookupMap , except that it stores additional data to be able to iterate through elements and supports dictionary-like operations such as keys(), values(), and items(). |
IterableMap | dict | An alias for UnorderedMap provided for compatibility with Rust SDK naming conventions. |
LookupSet | set | A non-iterable set of unique values. This structure cannot be iterated over and can only be used for membership testing. |
UnorderedSet | set | An iterable equivalent of LookupSet which stores additional metadata to allow iteration over the values in the set. |
IterableSet | set | An alias for UnorderedSet provided for compatibility with Rust SDK naming conventions. |
TreeMap | SortedDict | An ordered key-value store where keys are maintained in sorted order. Provides operations for range queries, finding nearest keys, and efficient min/max operations. |
Featuresβ
Type | Iterable | Clear All Values | Preserves Insertion Order | Range Selection |
---|---|---|---|---|
Vector | β | β | β | β |
LookupSet | ||||
UnorderedSet | β | β | β | |
IterableSet | β | β | β | |
LookupMap | ||||
UnorderedMap | β | β | β | |
IterableMap | β | β | β | |
TreeMap | β | β | β | β |
Complexityβ
Type | Access | Insert | Delete | Search | Traverse | Clear |
---|---|---|---|---|---|---|
Vector | O(1) | O(1)* | O(1)** | O(n) | O(n) | O(n) |
LookupSet | O(1) | O(1) | O(1) | O(1) | N/A | N/A |
UnorderedSet | O(1) | O(1) | O(1) | O(1) | O(n) | O(n) |
IterableSet | O(1) | O(1) | O(1) | O(1) | O(n) | O(n) |
LookupMap | O(1) | O(1) | O(1) | O(1) | N/A | N/A |
IterableMap | O(1) | O(1) | O(1) | O(1) | O(n) | O(n) |
TreeMap | O(1) | O(log n) | O(log n) | O(log n) | O(n) | O(n) |
* - to insert at the end of the vector using push_back
(or push_front
for deque)
** - to delete from the end of the vector using pop
(or pop_front
for deque), or delete using swap_remove
which swaps the element with the last element of the vector and then removes it.
SDK Collections Cookbookβ
Let's see how to use the SDK collections in practice
Instantiationβ
All structures need to be initialized using a unique prefix
, which will be used to index the collection's values in the account's state
- π JavaScript
- π¦ Rust
- π¦ Rust (legacy)
- π Python
Loading...
Do not forget to use the schema
to define how your contract's state is structured
Loading...
Notice how we use enums
to ensure all collections have a different prefix. Another advantage of using enums
is that they are serialized into a single byte
prefix.
Loading...
Notice how we use enums
to ensure all collections have a different prefix. Another advantage of using enums
is that they are serialized into a single byte
prefix.
from near_sdk_py import view, call, init
from near_sdk_py.collections import Vector, LookupMap, UnorderedMap, LookupSet, UnorderedSet
class MyContract:
@init
def new(self): # Create a Vector with prefix "v"
self.my_vector = Vector("v")
# Create a LookupMap with prefix "m"
self.my_lookup_map = LookupMap("m")
# Create an UnorderedMap with prefix "um"
self.my_unordered_map = UnorderedMap("um")
# Create a LookupSet with prefix "s"
self.my_lookup_set = LookupSet("s")
# Create an UnorderedSet with prefix "us"
self.my_unordered_set = UnorderedSet("us")
# For nested collections, use different prefixes
self.nested_maps = UnorderedMap("nested")
return True
@call
def create_nested_map(self, key: str):
# Create a new map that will be stored inside another map
inner_map = UnorderedMap(f"inner_{key}")
self.nested_maps[key] = inner_map
return {"success": True}
Be careful of not using the same prefix in two collections, otherwise, their storage space will collide, and you might overwrite information from one collection when writing in the other
Vectorβ
Implements a vector/array which persists in the contract's storage. Please refer to the Rust and JS SDK's for a full reference on their interfaces.
- π JavaScript
- π¦ Rust
- π¦ Rust (legacy)
- π Python
Loading...
Loading...
Loading...
from near_sdk_py import view, call, init
from near_sdk_py.collections import Vector
class VectorExample:
@init
def new(self):
# Create a Vector with prefix "v"
self.my_vector = Vector("v")
@call
def add_number(self, number):
# Append a value to the vector
self.my_vector.append(number)
return len(self.my_vector)
@view
def get_number(self, index):
# Get a value at specific index
try:
return self.my_vector[index]
except Exception:
return None
@view
def get_all_numbers(self):
# Convert entire vector to a list
return [num for num in self.my_vector]
LookupMapβ
Implements a map/dictionary which persists in the contract's storage. Please refer to the Rust and JS SDK's for a full reference on their interfaces.
- π JavaScript
- π¦ Rust