Skip to content
Learning Platform
Intermediate
30 minutes
GraphQL Schema Design Queries Subscriptions Filtering Pagination

Prerequisites:

  • 01-why-indexing

GraphQL для блокчейн-данных

Единый язык запросов для всех индексаторов

Все три индексатора (Subsquid, The Graph, SubQuery) выставляют данные через GraphQL API. GraphQL — это язык запросов к API, разработанный Facebook в 2012 году и опубликованный как open-source в 2015.

В отличие от REST (фиксированные endpoints с предопределённой формой ответа), GraphQL позволяет клиенту ТОЧНО указать какие данные нужны. Для блокчейн-данных это идеально: “Дай мне последние 20 Transfer событий с полями from, to, value, отсортированные по blockNumber.”

Прежде чем строить индексаторы, нужно понять язык, на котором мы будем с ними разговаривать.

GraphQL vs REST

REST подход (Module 4)

В REST вы работаете с фиксированными endpoints:

GET /api/transfers?limit=20&sort=blockNumber
GET /api/accounts/0xf39F...
GET /api/stats/volume

Три запроса к серверу. Каждый возвращает фиксированную форму данных — включая поля, которые вам не нужны (over-fetching). Или не включая поля, которые нужны (under-fetching — нужен ещё один запрос).

GraphQL подход

# Одним запросом: трансферы И аккаунты
query {
  transfers(limit: 5, orderBy: blockNumber_DESC) {
    from
    to
    value
  }
  accounts(limit: 3, orderBy: balance_DESC) {
    id
    balance
  }
}

Один запрос — получаете ровно те поля, которые указали. Не больше, не меньше. Два ресурса (transfers и accounts) в одном HTTP POST.

Ключевые различия

ХарактеристикаRESTGraphQL
EndpointsМножество (/transfers, /accounts)Один (/graphql)
Форма ответаФиксирована серверомОпределяется клиентом
Over-fetchingЧастый (лишние поля)Нет (выбираете поля)
Множественные ресурсыN запросов1 запрос
ТипизацияЗависит от реализацииВстроенная (schema)

В REST вы получаете то, что сервер решил отдать. В GraphQL — то, что ВАМ нужно.

Проектирование схемы

Проектирование GraphQL схемы для блокчейн-данных
Сущности (Entities)
type Transfer @entity {
  id: ID!
  from: String!
  to: String!
  value: BigInt!
  timestamp: DateTime!
  blockNumber: Int!
  txHash: String!
}
@entity = таблица в PostgreSQL. Каждое поле = колонка.
Связи (Relations)
type Account @entity {
  id: ID!
  balance: BigInt!
  transfersFrom: [Transfer!]
    @derivedFrom(field: "from")
  transfersTo: [Transfer!]
    @derivedFrom(field: "to")
}
@derivedFrom = обратная связь. Account.transfersFrom -- все трансферы ОТ этого аккаунта.
Индексы (@index)
type Transfer @entity {
  id: ID!
  from: String! @index
  to: String! @index
  blockNumber: Int! @index
}
@index = B-tree индекс в PostgreSQL. Ускоряет WHERE from = '0x...' запросы.
Кодогенерация из схемы:
schema.graphql
codegen
TypeORM entities
AS types
Единственный источник истиныschema.graphql -- единственный источник истины. И Subsquid, и The Graph генерируют код из этого файла.

Уровень 1: Интуитивный (аналогия)

schema.graphql — это чертёж базы данных. Как архитектор рисует план здания до начала строительства, вы описываете структуру данных до написания кода. Entities = таблицы. Fields = столбцы. Relations = foreign keys.

Entities и директивы

В экосистеме блокчейн-индексаторов schema.graphql — это SINGLE SOURCE OF TRUTH. И Subsquid, и The Graph генерируют код из этого файла:

# Каждый Transfer -- одно событие из блокчейна
type Transfer @entity {
  id: ID!                 # Уникальный ID (обязательное поле)
  from: String! @index    # Отправитель (@index для быстрого поиска)
  to: String! @index      # Получатель
  value: BigInt!          # Количество токенов (uint256)
  timestamp: DateTime!    # Время блока (ISO 8601)
  blockNumber: Int!       # Номер блока
  txHash: String! @index  # Хеш транзакции
}

# Агрегация: баланс каждого аккаунта
type Account @entity {
  id: ID!                 # Адрес аккаунта
  balance: BigInt!        # Текущий баланс
}

Ключевые директивы:

ДирективаНазначениеАналог в SQL
@entityЭтот тип = таблица в PostgreSQLCREATE TABLE
@indexСоздать B-tree индекс на полеCREATE INDEX
@derivedFromОбратная связь (не хранится в БД)JOIN
!Non-nullable (обязательное поле)NOT NULL

Типы данных для блокчейна

Стандартные скалярные типы GraphQL (String, Int, Float, Boolean, ID) недостаточны для блокчейн-данных. Индексаторы добавляют специальные типы:

ТипНазначениеПример
BigIntToken amounts (uint256)1000000000000000000 (1 ETH)
BytesАдреса и хеши (hex strings)0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
DateTimeВременные метки (ISO 8601)2024-01-15T12:30:00.000Z
ID!Уникальный идентификатор0001234567-000042-abcde

Важно: Стандартный JavaScript Number — 64-bit float. Максимум: 2^53 (~9 * 10^15). Token supply может быть 10^18 и больше. Поэтому BigInt ОБЯЗАТЕЛЕН для любых token amounts.

Запросы (Queries)

Построение GraphQL запросов для блокчейн-данных
Сущность:
Поля:
Фильтр (from_eq):
Сортировка:
Лимит:
Сгенерированный запрос:
query {
  transfers(
    orderBy: blockNumber_DESC,
    limit: 10,
    where: { from_eq: "0xa5f3...e7f8" }
  ) {
    from
    to
    value
    blockNumber
  }
}
Ответ (mock):
{
  "data": {
    "transfers": [
      { "from": "0xa5f3...e7f8", "to": "0xb2c4...b6c8", "value": "100000", "blockNumber": 19500123 },
      { "from": "0xd1e2...f3a4", "to": "0xa5f3...e7f8", "value": "50000", "blockNumber": 19500100 }
    ]
  }
}
Три типа запросовQuery -- получение данных (GET). Subscription -- подписка на изменения в реальном времени (WebSocket). Connection -- пагинация с totalCount и cursor (для больших наборов данных).

Базовый запрос

query {
  transfers {
    from
    to
    value
  }
}

Возвращает все трансферы с тремя полями. Просто и предсказуемо.

Фильтрация (where)

query {
  transfers(where: { from_eq: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" }) {
    to
    value
    blockNumber
  }
}

Доступные операторы фильтрации:

ОператорЗначениеПример
_eqРавноfrom_eq: "0x..."
_gtБольшеblockNumber_gt: 100
_gteБольше или равноvalue_gte: 1000
_ltМеньшеblockNumber_lt: 500
_lteМеньше или равноvalue_lte: 99999
_inВ спискеfrom_in: ["0x...", "0x..."]
_containsСодержит подстрокуfrom_contains: "f39fd"
_startsWithНачинается сtxHash_startsWith: "0xab"

Сортировка (orderBy)

query {
  transfers(orderBy: blockNumber_DESC) {
    from
    to
    value
    blockNumber
  }
}

Суффиксы: _ASC (по возрастанию), _DESC (по убыванию).

Пагинация (limit + offset)

# Страница 3 (элементы 41-60)
query {
  transfers(limit: 20, offset: 40, orderBy: blockNumber_DESC) {
    from
    to
    value
  }
}

Для cursor-based пагинации (более эффективно на больших наборах данных):

query {
  transfersConnection(first: 20, after: "cursor_value", orderBy: blockNumber_DESC) {
    edges {
      node {
        from
        to
        value
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

Вложенные запросы

query {
  accounts(limit: 5, orderBy: balance_DESC) {
    id
    balance
    # Если бы Transfer имел связь @derivedFrom с Account:
    # transfersFrom { to value }
  }
}

Подписки (Subscriptions)

WebSocket-based real-time обновления — одна из killer-features GraphQL для блокчейн-данных:

subscription {
  transfers(orderBy: blockNumber_DESC, limit: 5) {
    from
    to
    value
    blockNumber
  }
}

Подписка — это запрос, который АВТОМАТИЧЕСКИ обновляется при появлении новых данных. Вместо polling (повторных запросов каждые N секунд), сервер PUSH-ит обновления через WebSocket.

Как это работает

Клиент                   Сервер (GraphQL)
  |                           |
  |--- subscription {...} --->|   (WebSocket handshake)
  |                           |
  |<-- { data: [...] } -------|   (начальные данные)
  |                           |
  |   ... новый блок ...      |
  |                           |
  |<-- { data: [...] } -------|   (обновление)
  |                           |
  |   ... ещё блок ...        |
  |                           |
  |<-- { data: [...] } -------|   (обновление)

В LAB-07 наш фронтенд использует подписки для Live Event Feed — каждый новый Transfer появляется на dashboard мгновенно, без перезагрузки страницы.

GraphQL клиенты

Для подключения фронтенда к GraphQL API используются специализированные клиенты:

КлиентРазмер (gzip)WebSocketКэшированиеНаш выбор
urql17KBgraphql-wsDocument cacheДа
Apollo Client258KBsubscriptions-transport-wsNormalized cacheНет
graphql-request5KBНетНетНет

В LAB-07 фронтенд уже настроен с urql (17KB gzipped). urql предоставляет useQuery и useSubscription хуки для React, встроенную поддержку graphql-ws для подписок, и document cache для автоматического кэширования. Для учебного dashboard из 3-х views — идеальный выбор.

Алгоритмический уровень

Как GraphQL сервер обрабатывает запрос

function executeQuery(query, schema, data):
  parsedQuery = parse(query)              # Parse GraphQL string -> AST
  validatedQuery = validate(parsedQuery, schema)  # Check against schema
  result = resolve(validatedQuery, data)  # Fetch from PostgreSQL
  return { data: result }

На стороне сервера (Subsquid GraphQL Server / Graph Node), GraphQL запрос транслируется в SQL SELECT:

GraphQL:
  transfers(where: { from_eq: "0x..." }, orderBy: blockNumber_DESC, limit: 20)

SQL:
  SELECT from, to, value, block_number, tx_hash
  FROM transfer
  WHERE "from" = '0x...'
  ORDER BY block_number DESC
  LIMIT 20

Именно поэтому @index в schema.graphql так важен — он создаёт B-tree индекс в PostgreSQL, ускоряя WHERE и ORDER BY с O(N) до O(log N).

Математический уровень (сложность запросов)

ОперацияБез индексаС B-tree индексом
where: { from_eq }O(N) full scanO(log N) seek
orderBy: blockNumber_DESCO(N log N) sortO(1) already sorted
limit: 20O(N) + truncateO(20) early stop
КомбинацияO(N log N)O(log N + 20)

Где N — количество записей в таблице. При 1M Transfer записей: без индекса — сканирование миллиона строк; с индексом — 20 шагов по B-tree.

Практика

Попробуйте сами: После завершения INDEX-04 (Subsquid ERC-20 индексатор), запустите LAB-07 и откройте GraphQL playground по адресу http://localhost:4350/graphql.

Попробуйте запросы из этого урока:

  1. Получите все трансферы с сортировкой по блоку
  2. Отфильтруйте трансферы по отправителю
  3. Используйте пагинацию (limit + offset)
  4. Запустите подписку и отправьте новую транзакцию — наблюдайте обновление в реальном времени

Итоги

КонцепцияСутьЗначение
GraphQL vs RESTКлиент выбирает поля, один endpointТочные запросы, нет over-fetching
schema.graphqlSingle source of truth для данныхГенерация кода и API
BigIntДля token amounts (uint256)JS Number недостаточен (2^53 max)
Фильтрация_eq, _gt, _lt, _in, _containsТочный поиск по любому полю
ПодпискиWebSocket real-time обновленияLive data без polling

Что дальше: В INDEX-03 мы разберём архитектуру Subsquid — как именно устроен наш primary индексатор: EvmBatchProcessor, TypeORM Store, GraphQL Server и конвейер кодогенерации. Это подготовит вас к первому hands-on упражнению в INDEX-04.

Finished the lesson?

Mark it as complete to track your progress