Yesterday: mappings are just hash lookups.
Today: why this design, how to track keys (on and off-chain)
There's no "directory" of which slots are used. The EVM only knows: "read slot X" or "write slot X."
Why no iteration?
To iterate, you'd need a list of keys. But storing that list means:
Extra SSTORE per insert (~20,000 gas)
Growing array = more slots = more cost
Deletion becomes expensive (swap-and-pop or leave gaps)
Mappings chose: O(1) access, zero overhead, no iteration.
When to track keys on-chain
Track on-chain when:
Contract logic requires iteration (distribute to all holders, enumerate NFTs), small, bounded sets (< 100 items typical), writes are infrequent
```solidity
mapping(address => uint256) public balances;
address[] public holders; // explicit key tracking
function deposit() external payable {
if (balances[msg.sender] == 0) {
holders.push(msg.sender); // first deposit = new key
}
balances[msg.sender] = msg.value;
}```
Track off-chain when:
Large/unbounded sets, iteration only needed by frontends/indexers, gas optimization is critical
Emit events, index with The Graph or your own indexer:
```solidity
event KeyAdded(address indexed key);
function deposit() external payable {
if (balances[msg.sender] == 0) {
emit KeyAdded(msg.sender); // ~375 gas vs ~20,000 for SSTORE
}
balances[msg.sender] = msg.value;
}```