4 минуты
Лучшие практики для субграфов №3 – Улучшение индексирования и производительности запросов с использованием неизменяемых объектов и байтов в качестве идентификаторов
Краткое содержание
Использование неизменяемых объектов и байтов в качестве идентификаторов в файле schema.graphql
значительно улучшает скорость индексирования и производительность запросов.
Неизменяемые объекты
Чтобы сделать объект неизменяемым, просто добавьте (immutable: true)
к объекту.
1type Transfer @entity(immutable: true) {2 id: Bytes!3 from: Bytes!4 to: Bytes!5 value: BigInt!6}
Сделав объект Transfer
неизменяемым, graph-node сможет обрабатывать его более эффективно, что улучшит скорость индексирования и отклик на запросы.
Структуры неизменяемых объектов не будут изменяться в будущем. Идеальным кандидатом для превращения в неизменяемый объект может быть объект, который напрямую фиксирует данные событий в блокчейне, например, событие Transfer
, записываемое как объект Transfer
.
Как это устроено
Изменяемые объекты имеют «диапазон блоков», указывающий их актуальность. Обновление таких объектов требует от graph node корректировки диапазона блоков для предыдущих версий, что увеличивает нагрузку на базу данных. Запросы также должны фильтровать данные, чтобы находить только актуальные объекты. Неизменяемые объекты работают быстрее, поскольку все они актуальны, и, так как они не изменяются, не требуется никаких проверок или обновлений при записи, а также фильтрации во время выполнения запросов.
Когда не следует использовать неизменяемые объекты
Если у Вас есть поле, такое как status
, которое необходимо изменять с течением времени, то не следует делать объект неизменяемым. В противном случае, используйте неизменяемые объекты, когда это возможно.
Использование Bytes в качестве идентификаторов
Каждый объект требует уникального идентификатора. В предыдущем примере мы видим, что идентификатор уже имеет тип Bytes.
1type Transfer @entity(immutable: true) {2 id: Bytes!3 from: Bytes!4 to: Bytes!5 value: BigInt!6}
Хотя для идентификаторов возможны и другие типы, такие как String и Int8, рекомендуется использовать тип Bytes для всех идентификаторов. Это связано с тем, что строковые данные занимают в два раза больше места, чем строковые данные в формате Byte, а сравнение строк в кодировке UTF-8 требует учета локали, что намного более затратно по сравнению с побайтовым сравнением, используемым для строк типа Byte.
Причины, по которым не стоит использовать Bytes как идентификаторы
- Если идентификаторы объектов должны быть читаемыми для человека, например, автоинкрементированные числовые идентификаторы или читаемые строки, то не следует использовать тип Bytes для идентификаторов.
- If integrating a Subgraph’s data with another data model that does not use Bytes as IDs, Bytes as IDs should not be used.
- Если улучшения производительности индексирования и запросов не являются приоритетом.
Конкатенация (объединение) с использованием Bytes как идентификаторов
It is a common practice in many Subgraphs to use string concatenation to combine two properties of an event into a single ID, such as using event.transaction.hash.toHex() + "-" + event.logIndex.toString()
. However, as this returns a string, this significantly impedes Subgraph indexing and querying performance.
Вместо этого следует использовать метод concatI32()
для конкатенации свойств события. Эта стратегия приводит к созданию идентификатора типа Bytes
, который гораздо более производителен.
1export function handleTransfer(event: TransferEvent): void {2 let entity = new Transfer(event.transaction.hash.concatI32(event.logIndex.toI32()))3 entity.from = event.params.from4 entity.to = event.params.to5 entity.value = event.params.value67 entity.blockNumber = event.block.number8 entity.blockTimestamp = event.block.timestamp9 entity.transactionHash = event.transaction.hash1011 entity.save()12}
Сортировка с использованием идентификаторов Bytes
Сортировка с использованием идентификаторов Bytes не является оптимальной, как это видно из примера запроса и ответа.
Запрос:
1{2 transfers(first: 3, orderBy: id) {3 id4 from5 to6 value7 }8}
Ответ на запрос:
1{2 "data": {3 "transfers": [4 {5 "id": "0x00010000",6 "from": "0xabcd...",7 "to": "0x1234...",8 "value": "256"9 },10 {11 "id": "0x00020000",12 "from": "0xefgh...",13 "to": "0x5678...",14 "value": "512"15 },16 {17 "id": "0x01000000",18 "from": "0xijkl...",19 "to": "0x9abc...",20 "value": "1"21 }22 ]23 }24}
Идентификаторы возвращаются в виде шестнадцатеричной строки.
Чтобы улучшить сортировку, мы должны создать другое поле в объекте, которое будет иметь тип BigInt.
1type Transfer @entity {2 id: Bytes!3 from: Bytes! # address4 to: Bytes! # address5 value: BigInt! # unit2566 tokenId: BigInt! # uint2567}
Это позволит оптимизировать сортировку в последовательном порядке.
Запрос:
1{2 transfers(first: 3, orderBy: tokenId) {3 id4 tokenId5 }6}
Ответ на запрос:
1{2 "data": {3 "transfers": [4 {5 "id": "0x…",6 "tokenId": "1"7 },8 {9 "id": "0x…",10 "tokenId": "2"11 },12 {13 "id": "0x…",14 "tokenId": "3"15 }16 ]17 }18}
Заключение
Using both Immutable Entities and Bytes as IDs has been shown to markedly improve Subgraph efficiency. Specifically, tests have highlighted up to a 28% increase in query performance and up to a 48% acceleration in indexing speeds.
Читайте больше о применении неизменяемых объектов и Bytes как идентификаторов в этом блоге от Дэвида Луттеркорта, инженера-программиста в Edge & Node: Два простых способа улучшить производительность субграфов.