GraphQL Joins
Starting March 2025, Bitquery APIs support joins on the v2 and EAP endpoint.
The joinPLACEHOLDER
function enables you to embed a subquery within your main query, allowing data retrieval from the same or a different cube. This is functionally equivalent to an SQL JOIN
statement, providing more efficient and structured data fetching.
For example,
query MyQuery {
EVM {
DEXTradeByTokens{
# fields from main cube
joinCalls{
# Additional fields from the joined cube
}
}
}
}
JOIN Type
4 types of joins are supported:
left
( default ) that returns all results from the query matched with all results from joined query. In case join query has no matching result, empty values are returnedany
is the same as left, except that maximum one ( any ) result is returned from the joined query;inner
returns only matching results. If there are no matching results in joined query, the result is not returned;inner_any
returns only one ( any ) matching result. If there are no matching results in joined query, the result is not returned;
Here are the additional details from your document that you may want to include in your Markdown:
1. Schemas Supported
- Joins are available for EVM, Tron, and Solana schemas in GraphQL v2 and EAP.
2. Join Query Structure
- The
joinPLACEHOLDER
function embeds a subquery into the main query, wherePLACEHOLDER
is the name of the cube being joined. - The joined query preserves the full schema of the joined cube, allowing:
- Querying all fields & metrics
- Using additional filters
- Setting limits & aggregations
3. Matching Conditions
At least one attribute must be selected for matching between the main query and joined query.
Example:
query {
EVM {
Transfers {
joinCalls(join: left, Call_To: Transfer_Receiver) {
count
}
}
}
}The above example joins
Calls.Call_To
withTransfers.Transfer_Receiver
.Multiple Matching Conditions
query {
EVM {
Transfers {
joinCalls(
join: left
Call_To: Transfer_Receiver
Transaction_Hash: Transaction_Hash
) {
count
}
}
}
}- Ensures both
Call_To
matchesTransfer_Receiver
andTransaction_Hash
matches.
- Ensures both
4. Other Attributes of Join Query
where
: Additional filteringlimit / limitBy
: Restricting result setsorderBy
: Sorting the joined resultsExample:
Transfers {
joinCalls(join: left Call_To: Transfer_Receiver
where: {
Call: {Signature: {Name: {in: ["Transfer","TransferFrom"]}}}
}
){
count
}
}
5. Performance Optimization
- Use joins only when necessary, as they are computationally expensive.
- Avoid unnecessary joins when the same data can be retrieved via direct queries.
- Use pre-aggregated results to reduce data load.
- Use join types
any
orinner_any
to limit excess data retrieval.
6. Limitations
- Joins only work in queries (subscriptions not supported).
- Cannot join different datasets (say real-time and archive).
- Joins can only be applied at the first query level.
- Cannot filter query results using join query fields.
7. Example Use Cases
Example 1 : Check if an address is a smartcontract
Take this query for example, it helps you detect if an address is a smart contract.
{
EVM(dataset: archive, network: eth) {
Transfers(
where: {Transfer: {Sender: {is: "0xcf38be613203b39a14d2fb3c1a345122ec0a4351"}}, Block: {Date: {after: "2025-03-01"}}}
) {
Transfer {
Receiver
}
count
joinCalls(Call_To: Transfer_Receiver, join: inner) {
count
}
}
}
}
How This Works
- The query finds all transfers from a specific sender
- Then, it checks if the receivers of these transfers were later called as smart contracts.
- Since only smart contracts can process function calls, it is likely a smart contract if an address appears in joinCalls.(since EOAs cannot process function calls)
- The count in joinCalls shows how many times the receiver was called.
Example 2: Get trades, volume and marketcap of a token
This query is a good example of how joins could be used to get mulltiple trade related matrixes with a single query.
query MyQuery($time_1hr: DateTime) {
Solana {
DEXTradeByTokens(
where: {Trade: {Currency: {MintAddress: {is: "83vzRC3B9EQVjz8NDULhn7ywcX16TD8FsVFUAEE7pump"}}}, Block: {Time: {since: $time_1hr}}}
limitBy: {by: Trade_Side_Currency_MintAddress, count: 1}
) {
volume: sum(of: Trade_Side_AmountInUSD)
trades: count
joinBalanceUpdates(
BalanceUpdate_Currency_MintAddress: Trade_Currency_MintAddress
orderBy: {descending: Block_Time}
) {
BalanceUpdate {
PostBalanceInUSD
PostBalance
Currency {
Name
MintAddress
Symbol
}
}
}
}
}
}
How this works
- The query finds all the trades for the particular token after a given timestamp.
- Then the query perform aggregates functions like
sum
andcount
to getvolume
andtrades
of a token after a given time. - Then it checks for the latest
BalanceUpdates
for the token. - The
PostBalance
implies thetotal supply
of a token andPostBalanceInUSD
implies the marketcap of the token.
Example 3: Get latest price and liquidity of a token in token pair
This query is a good example of how joins could be used to get latest price and liquidity of a token in particular token pair.
query MyQuery {
EVM(dataset: combined, network: eth) {
BalanceUpdates(
where: {BalanceUpdate: {Address: {is: "0x1bCd6B0E97B51D76FD1752111a1fe2b473F655eE"}},
Currency: {SmartContract: {is: "0x6b175474e89094c44da98b954eedeac495271d0f"}}}
) {
dai_liq: sum(of: BalanceUpdate_Amount)
joinDEXTradeByTokens(
Trade_Currency_SmartContract: Currency_SmartContract
orderBy: {descending: Block_Time}
limit: {count: 1}
) {
Trade {
PriceInUSD
}
}
}
}
}
How this works
- The query finds all the balance update records for the particular token and a particular token pair address.
- Then it sums up the amount from all
Balance Updates
to get the amount of tokens currently in the pool. - Then it checks the latest
DEX Trades
for the mentioned token. - The
PriceInUSD
in joinDEXTradeByTokens shows the latest price of the token.