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 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 all together, and SDK collections for large amounts of data that do not need to be accessed all together.
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)
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 contairn 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. |
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)
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.
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)
Loading...
Loading...
Loading...
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
Loading...
Loading...
UnorderedMap / IterableMap
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
Loading...
Loading...
LookupSet
Implements a set 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
Loading...
Loading...
UnorderedSet / IterableSet
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
Loading...
Loading...