Skip to content

[Performance] Avoid N+1 remote table enumeration in external session bypass SHOW TABLES path #66025

Open

Description

@wenzhenghu

Summary

In the external session-bypass path, ExternalDatabase.getTables() performs N+1 remote table-name enumerations.

When extCatalog.shouldBypassTableNameCache(sessionContext) is true:

  1. getTables() calls getTableNamesWithLock(), which enumerates remote table names once via listLocalTableNamesWithoutCache(sessionContext).
  2. Then getTables() iterates all table names and calls getTableNullable(tblName).
  3. In bypass mode, getTableNullable(tblName) enters getTableNullableWithoutCache(sessionContext, tableName).
  4. getTableNullableWithoutCache(...) calls findTableNamePairWithoutCache(...), which again does a full listTableNames(sessionContext) scan for each table.

As a result, listing N tables triggers 1 + N remote LIST TABLES enumerations.

Affected code

fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalDatabase.java

Current structure:

public List<T> getTables() {
    Set<String> tblNames = getTableNamesWithLock();   // 1 full remote enumeration in bypass mode
    for (String tblName : tblNames) {
        T tbl = getTableNullable(tblName);            // each iteration re-enters bypass path
    }
}

public Set<String> getTableNamesWithLock() {
    if (extCatalog.shouldBypassTableNameCache(sessionContext)) {
        return Sets.newHashSet(listLocalTableNamesWithoutCache(sessionContext));
    }
}

private T getTableNullableWithoutCache(SessionContext sessionContext, String tableName) {
    Optional<Pair<String, String>> matched = findTableNamePairWithoutCache(sessionContext, tableName);
    // findTableNamePairWithoutCache() -> listTableNames(sessionContext)
}

Impact

This affects session-bypass catalogs such as delegated Iceberg REST session catalogs.

For SHOW TABLES / getTables() on a database with many tables, the current implementation scales poorly because every table object construction repeats a full remote name enumeration.

The issue is performance-only, but the overhead can become significant for large databases or expensive remote catalogs.

Scope / origin

This is a pre-existing master/baseline issue, not introduced by PR #65126.

The bypass structure already existed before the external meta cache refactor. PR #65126 carried it forward and added related session-bypass improvements elsewhere, but did not address this getTables() path.

Suggested fix

In bypass mode, reuse a single remote name enumeration inside getTables():

  • enumerate (remoteName, localName) pairs once,
  • build a local lookup map from the result,
  • construct table objects directly from that snapshot instead of re-calling getTableNullable() for every table.

This keeps bypass semantics unchanged while removing the repeated full scans.

Related

Activity

  1. u70b3 commented on Aug 4, 2026

    @u70b3
    Contributor

    Hi, I'd like to pick this up. The N+1 comes from getTables() calling getTableNamesWithLock() (one remote enumeration) and then getTableNullable() per table, which in bypass mode re-enters findTableNamePairWithoutCache() → a full listTableNames() per table.

    Fix: in the bypass branch, enumerate once via listTableNames(sessionContext) and build all tables from that single snapshot, keeping the case-insensitive conflict check and the first-win dedup semantics.

    Local verification: a unit test counting remote listings shows 4 enumerations for 3 tables before the fix and exactly 1 after. Will open a PR shortly.

  2. added 7 commits that reference this issue on Aug 4, 2026
    e4df0e3
    c645baa
    7332ab7
    2ffa5c5
    fe6d568
    0341c77
    a068067
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions