This is an automated email from the ASF dual-hosted git repository.

yuqi1129 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new db0dbdedf7 [#13582] improvement(core): Make JDBC idle connection 
retention configurable (#13620)
db0dbdedf7 is described below

commit db0dbdedf7f7c726e698ff75294f28c90bde40b1
Author: Qi Yu <[email protected]>
AuthorDate: Fri Oct 9 11:22:56 2026 +0800

    [#13582] improvement(core): Make JDBC idle connection retention 
configurable (#13620)
    
    ### What changes were proposed in this pull request?
    
    Make JDBC idle-connection retention configurable for the entity-store
    pool and for JDBC catalog pools.
    
    - Entity store: add
    `gravitino.entity.store.relational.maxIdleConnections` (default `10`,
    the previous hard-coded value, capped by `maxConnections`) in place of
    the hard-coded `maxIdle = 10` in `SqlSessionFactoryHelper`. `minIdle` is
    capped by the effective `maxIdle`. The Helm chart exposes it as
    `entity.maxIdleConnections`.
    - JDBC catalogs: add the catalog property `jdbc.pool.max-idle` (default
    `8`, DBCP's previous default, capped by `jdbc.pool.max-size`). When it
    is not set, a `maxIdle` supplied through `gravitino.bypass.` is still
    honored; when both are set, `jdbc.pool.max-idle` wins.
    - Document both settings, including that connections are reused only up
    to the idle limit, that entity-store idle connections are released
    slowly (the evictor checks at most three idle connections every ten
    minutes) and that catalog pools do not evict idle connections by
    default.
    - Add `dev/performance/jdbc_idle_retention.py`, a script that measures
    MySQL connection churn and HTTP tail latency under concurrent reads.
    
    ### Why are the changes needed?
    
    With `maxConnections = 100` but `maxIdle = 10`, a 64-client
    metalake-list workload repeatedly destroyed and recreated database
    connections. The measurements in #13582 compare `maxIdle = 10` with
    `maxIdle = 100`: about 3,400 physical connections per measurement window
    versus 4–36, and a median p99 of 214 ms versus 90 ms. The idle limit was
    not configurable.
    
    The churn comes from the pool closing a returned connection whenever it
    already holds `maxIdle` idle connections; idle-time eviction plays no
    part. Churn continues while the number of connections in use swings by
    more than the limit, so the limit should be at or above the steady peak
    concurrency.
    
    Measured on PR head e1e1bc9cc5 with
    `dev/performance/jdbc_idle_retention.py`: single server, MySQL 8.0.35,
    512 MiB heap, 73 metalakes, 64 keep-alive clients on `GET
    /api/metalakes`, a fresh server per setting, 3 rounds of 15 s each, run
    in the order 10, 32, 64, 100, 32, 10. No request failed. Round 1
    includes server warm-up, so the p99 column uses rounds 2-3.
    
    | `maxIdleConnections` | New MySQL connections per round | p99, rounds
    2-3 (ms) | Throughput (req/s) |
    |---|---|---|---|
    | 10 (two runs) | 1,367-2,726 | 211-234 | 2,000-3,400 |
    | 32 (two runs) | 272-439 | 185-215 | 3,080-4,540 |
    | 64 | 1-2 | 163-180 | 2,080-4,100 |
    | 100 | 1-16 | 151-181 | 3,430-4,460 |
    
    The default stays at `10`. Retained idle connections are multiplied by
    the number of servers (typically three or more), and they are released
    slowly, so a higher default would raise the connection budget of every
    deployment. Deployments with high read concurrency can raise the limit
    toward their peak concurrency within their database connection budget.
    
    Out of scope, left for follow-ups: the DBCP pools of
    `JdbcAuthorizationPlugin` (fixed `maxIdle = 5`) and of JDBC partition
    statistics storage (`JdbcDataSourceFactory`, DBCP's default `8`) keep
    their fixed idle limits; pool created/destroyed metrics for diagnosing
    churn (MySQL's `Connections` counter and the script cover it for now),
    and validating catalog pool properties before datasource creation so an
    invalid value is reported by name rather than as "Error creating
    datasource" (an existing behavior shared with `jdbc.pool.max-size`).
    Both new settings accept only positive values on purpose; a catalog that
    needs DBCP's `0` or `-1` can still use `gravitino.bypass.maxIdle`.
    
    Fix: #13582
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes.
    
    - New server configuration
    `gravitino.entity.store.relational.maxIdleConnections`. Its default
    equals the previous hard-coded value `10`, so existing deployments are
    unchanged. Idle connections are released slowly, so budget for the full
    value on every server when raising it.
    - New JDBC catalog property `jdbc.pool.max-idle`. Its default equals the
    previous behavior, so existing catalogs are unchanged.
    - New Helm value `entity.maxIdleConnections` (unset by default).
    
    ### How was this patch tested?
    
    - Unit tests for the new settings, their caps and defaults, and the
    `gravitino.bypass.maxIdle` precedence (tested at the `DataSourceUtils`
    level; the prefix stripping in `JdbcCatalogOperations.initialize` is
    unchanged): `TestSqlSession`, `TestConfig`, `TestJdbcConfig`,
    `TestJdbcCatalogOperations` (including a burst test that checks physical
    connections are reused), and `TestJdbcCatalogPropertiesMetadata`.
    - Every test class that stubs the entity-store config, plus the full
    `catalog-jdbc-common` and `idp-basic` suites, pass locally. Reverting
    the `SqlSessionFactoryHelper` change makes
    `testConfiguredMaxIdleAndPoolCap` and `testMinIdleCappedByMaxIdle` fail;
    reverting the `DataSourceUtils` change makes `testConfigMaxIdle` and
    `testRetainsConnectionsAcrossReadBursts` fail.
    - `:core:compileJmhJava`, javadoc for `core` and `catalog-jdbc-common`,
    and `spotlessApply`. The Helm chart tests are covered by CI
    (`chart-test`); Helm is not installed locally.
    - The benchmark script's self-test: `cd dev/performance && python3 -m
    unittest test_jdbc_idle_retention`. It is a developer tool and is not
    run in CI on purpose.
    - `minIdle` is capped by the effective `maxIdle` so the DBCP getters
    report the value the pool actually uses; commons-pool2 applies the same
    cap internally, so behavior is unchanged.
    `TestSqlSession.testMinIdleCappedByMaxIdle` and
    `TestJdbcCatalogOperations.testConfigMaxIdle` pin the cap.
    
    ---------
    
    Signed-off-by: yuqi <[email protected]>
---
 .../fileset/TestFilesetCatalogOperations.java      |   2 +
 .../jdbc/JdbcCatalogPropertiesMetadata.java        |   9 +
 .../gravitino/catalog/jdbc/config/JdbcConfig.java  |  21 +++
 .../catalog/jdbc/utils/DataSourceUtils.java        |  12 +-
 .../catalog/jdbc/TestJdbcCatalogOperations.java    |  91 ++++++++++
 .../jdbc/TestJdbcCatalogPropertiesMetadata.java    |   8 +
 .../catalog/jdbc/config/TestJdbcConfig.java        |  17 ++
 .../catalog/kafka/TestKafkaCatalogOperations.java  |   2 +
 .../generic/TestGenericCatalogOperations.java      |   2 +
 .../catalog/model/TestModelCatalogOperations.java  |   2 +
 conf/gravitino.conf.template                       |   3 +
 .../cache/it/AbstractEntityStorageBenchmark.java   |   4 +
 .../main/java/org/apache/gravitino/Configs.java    |  16 ++
 .../session/SqlSessionFactoryHelper.java           |  12 +-
 .../test/java/org/apache/gravitino/TestConfig.java |  14 ++
 .../authorization/TestAccessControlManager.java    |   2 +
 .../gravitino/authorization/TestOwnerManager.java  |   2 +
 .../gravitino/hook/TestFilesetHookDispatcher.java  |   3 +
 .../apache/gravitino/policy/TestPolicyManager.java |   2 +
 .../gravitino/stats/TestStatisticManager.java      |   2 +
 .../storage/AbstractEntityStorageTest.java         |   2 +
 .../storage/relational/BackendTestExtension.java   |   2 +
 ...TestRelationalEntityStoreHierarchicalCache.java |   2 +
 .../mapper/provider/base/TestAuthMappers.java      |   2 +
 .../provider/base/TestEntityChangeLogMapper.java   |   2 +
 .../provider/base/TestUserRoleRelMapper.java       |   2 +
 .../storage/relational/session/TestSqlSession.java |  64 +++++++
 .../org/apache/gravitino/tag/TestTagManager.java   |   2 +
 .../gravitino/resources/config/gravitino.conf      |   3 +
 dev/charts/gravitino/tests/configmap_test.yaml     |  19 ++
 dev/charts/gravitino/values.yaml                   |   5 +
 dev/performance/jdbc_idle_retention.py             | 195 +++++++++++++++++++++
 dev/performance/test_jdbc_idle_retention.py        |  77 ++++++++
 docs/gravitino-server-config.md                    |  29 +--
 docs/how-to-use-relational-backend-storage.md      |  21 +++
 docs/jdbc-clickhouse-catalog.md                    |  19 +-
 docs/jdbc-doris-catalog.md                         |  19 +-
 docs/jdbc-hologres-catalog.md                      |  19 +-
 docs/jdbc-mysql-catalog.md                         |  19 +-
 docs/jdbc-oceanbase-catalog.md                     |  19 +-
 docs/jdbc-postgresql-catalog.md                    |  21 +--
 docs/jdbc-starrocks-catalog.md                     |  19 +-
 42 files changed, 705 insertions(+), 83 deletions(-)

diff --git 
a/catalogs/catalog-fileset/src/test/java/org/apache/gravitino/catalog/fileset/TestFilesetCatalogOperations.java
 
b/catalogs/catalog-fileset/src/test/java/org/apache/gravitino/catalog/fileset/TestFilesetCatalogOperations.java
index eeee30469d..6a9c49dfdd 100644
--- 
a/catalogs/catalog-fileset/src/test/java/org/apache/gravitino/catalog/fileset/TestFilesetCatalogOperations.java
+++ 
b/catalogs/catalog-fileset/src/test/java/org/apache/gravitino/catalog/fileset/TestFilesetCatalogOperations.java
@@ -24,6 +24,7 @@ import static 
org.apache.gravitino.Configs.ENTITY_CHANGE_LOG_POLL_INTERVAL_SECS;
 import static org.apache.gravitino.Configs.ENTITY_CHANGE_LOG_RETENTION_SECS;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS;
+import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_PASSWORD;
 import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_PATH;
 import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL;
@@ -267,6 +268,7 @@ public class TestFilesetCatalogOperations {
     
when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_PASSWORD)).thenReturn("gravitino");
     
when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER)).thenReturn("org.h2.Driver");
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS)).thenReturn(100);
+    
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS)).thenReturn(10);
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS)).thenReturn(1000L);
 
     File f = FileUtils.getFile(STORE_PATH);
diff --git 
a/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/JdbcCatalogPropertiesMetadata.java
 
b/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/JdbcCatalogPropertiesMetadata.java
index ebb6a1bf31..062dd43888 100644
--- 
a/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/JdbcCatalogPropertiesMetadata.java
+++ 
b/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/JdbcCatalogPropertiesMetadata.java
@@ -45,6 +45,7 @@ public class JdbcCatalogPropertiesMetadata extends 
BaseCatalogPropertiesMetadata
           JdbcConfig.PASSWORD.getKey(),
           JdbcConfig.POOL_MIN_SIZE.getKey(),
           JdbcConfig.POOL_MAX_SIZE.getKey(),
+          JdbcConfig.POOL_MAX_IDLE.getKey(),
           JdbcConfig.TEST_ON_BORROW.getKey());
 
   static {
@@ -104,6 +105,14 @@ public class JdbcCatalogPropertiesMetadata extends 
BaseCatalogPropertiesMetadata
                 JdbcConfig.POOL_MAX_SIZE.getDefaultValue(),
                 false /* hidden */,
                 false /* reserved */),
+            integerPropertyEntry(
+                JdbcConfig.POOL_MAX_IDLE.getKey(),
+                JdbcConfig.POOL_MAX_IDLE.getDoc(),
+                false /* required */,
+                false /* immutable */,
+                JdbcConfig.POOL_MAX_IDLE.getDefaultValue(),
+                false /* hidden */,
+                false /* reserved */),
             booleanPropertyEntry(
                 JdbcConfig.TEST_ON_BORROW.getKey(),
                 JdbcConfig.TEST_ON_BORROW.getDoc(),
diff --git 
a/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/config/JdbcConfig.java
 
b/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/config/JdbcConfig.java
index e474bcda39..753a9fac84 100644
--- 
a/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/config/JdbcConfig.java
+++ 
b/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/config/JdbcConfig.java
@@ -87,6 +87,18 @@ public class JdbcConfig extends Config {
           .checkValue(value -> value > 0, 
ConfigConstants.POSITIVE_NUMBER_ERROR_MSG)
           .createWithDefault(10);
 
+  /**
+   * The maximum number of idle connections a catalog pool retains. The 
default matches the DBCP
+   * default that catalog pools used before this setting existed.
+   */
+  public static final ConfigEntry<Integer> POOL_MAX_IDLE =
+      new ConfigBuilder("jdbc.pool.max-idle")
+          .doc("The maximum number of idle connections retained in the pool")
+          .version(ConfigConstants.VERSION_2_0_0)
+          .intConf()
+          .checkValue(value -> value > 0, 
ConfigConstants.POSITIVE_NUMBER_ERROR_MSG)
+          .createWithDefault(8);
+
   public static final ConfigEntry<Boolean> TEST_ON_BORROW =
       new ConfigBuilder("jdbc.pool.test-on-borrow")
           .doc("Whether to test the connection on borrow")
@@ -126,6 +138,15 @@ public class JdbcConfig extends Config {
     return get(POOL_MAX_SIZE);
   }
 
+  /**
+   * Returns the effective idle-connection limit, capped by the maximum pool 
size.
+   *
+   * @return the maximum number of idle connections to retain
+   */
+  public int getPoolMaxIdle() {
+    return Math.min(get(POOL_MAX_IDLE), getPoolMaxSize());
+  }
+
   public String getJdbcDatabase() {
     return get(JDBC_DATABASE);
   }
diff --git 
a/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/utils/DataSourceUtils.java
 
b/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/utils/DataSourceUtils.java
index 7a7847ebf5..91c4a6bb4e 100644
--- 
a/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/utils/DataSourceUtils.java
+++ 
b/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/utils/DataSourceUtils.java
@@ -152,7 +152,17 @@ public class DataSourceUtils {
     String password = jdbcConfig.getPassword();
     basicDataSource.setPassword(password);
     basicDataSource.setMaxTotal(jdbcConfig.getPoolMaxSize());
-    basicDataSource.setMinIdle(jdbcConfig.getPoolMinSize());
+    // Preserve maxIdle supplied through the existing DBCP bypass unless the 
canonical setting is
+    // explicitly configured. The canonical default applies to catalogs 
without either setting.
+    if 
(jdbcConfig.getAllConfig().containsKey(JdbcConfig.POOL_MAX_IDLE.getKey())
+        || !jdbcConfig.getAllConfig().containsKey("maxIdle")) {
+      basicDataSource.setMaxIdle(jdbcConfig.getPoolMaxIdle());
+    }
+    int maxIdle = basicDataSource.getMaxIdle();
+    basicDataSource.setMinIdle(
+        maxIdle >= 0
+            ? Math.min(jdbcConfig.getPoolMinSize(), maxIdle)
+            : jdbcConfig.getPoolMinSize());
     // Set each time a connection is taken out from the connection pool, a 
test statement will be
     // executed to confirm whether the connection is valid.
     basicDataSource.setTestOnBorrow(jdbcConfig.getTestOnBorrow());
diff --git 
a/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/TestJdbcCatalogOperations.java
 
b/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/TestJdbcCatalogOperations.java
index a070f101c6..abfe47cf0a 100644
--- 
a/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/TestJdbcCatalogOperations.java
+++ 
b/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/TestJdbcCatalogOperations.java
@@ -19,11 +19,18 @@
 package org.apache.gravitino.catalog.jdbc;
 
 import com.google.common.collect.Maps;
+import java.sql.Connection;
 import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.IdentityHashMap;
 import java.util.List;
+import java.util.Set;
 import javax.sql.DataSource;
 import org.apache.commons.dbcp2.BasicDataSource;
+import org.apache.commons.dbcp2.DelegatingConnection;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.catalog.jdbc.config.JdbcConfig;
 import 
org.apache.gravitino.catalog.jdbc.converter.SqliteColumnDefaultValueConverter;
@@ -81,6 +88,90 @@ public class TestJdbcCatalogOperations {
     ((BasicDataSource) dataSource).close();
   }
 
+  @Test
+  public void testConfigMaxIdle() throws SQLException {
+    HashMap<String, String> properties = Maps.newHashMap();
+    properties.put(JdbcConfig.JDBC_DRIVER.getKey(), "org.sqlite.JDBC");
+    properties.put(JdbcConfig.JDBC_URL.getKey(), "jdbc:sqlite::memory:");
+    properties.put(JdbcConfig.USERNAME.getKey(), "test");
+    properties.put(JdbcConfig.PASSWORD.getKey(), "test");
+    properties.put(JdbcConfig.POOL_MAX_SIZE.getKey(), "16");
+    properties.put(JdbcConfig.POOL_MAX_IDLE.getKey(), "12");
+
+    try (BasicDataSource dataSource =
+        (BasicDataSource) DataSourceUtils.createDataSource(properties)) {
+      Assertions.assertEquals(16, dataSource.getMaxTotal());
+      Assertions.assertEquals(12, dataSource.getMaxIdle());
+    }
+
+    properties.remove(JdbcConfig.POOL_MAX_IDLE.getKey());
+    properties.put("maxIdle", "7");
+    try (BasicDataSource dataSource =
+        (BasicDataSource) DataSourceUtils.createDataSource(properties)) {
+      Assertions.assertEquals(7, dataSource.getMaxIdle());
+    }
+
+    properties.put(JdbcConfig.POOL_MAX_IDLE.getKey(), "12");
+    try (BasicDataSource dataSource =
+        (BasicDataSource) DataSourceUtils.createDataSource(properties)) {
+      Assertions.assertEquals(12, dataSource.getMaxIdle());
+    }
+
+    properties.remove(JdbcConfig.POOL_MAX_IDLE.getKey());
+    properties.put("maxIdle", "-1");
+    try (BasicDataSource dataSource =
+        (BasicDataSource) DataSourceUtils.createDataSource(properties)) {
+      Assertions.assertEquals(-1, dataSource.getMaxIdle());
+      Assertions.assertEquals(2, dataSource.getMinIdle());
+    }
+
+    // An idle limit below the minimum pool size also caps minIdle.
+    properties.remove("maxIdle");
+    properties.put(JdbcConfig.POOL_MAX_IDLE.getKey(), "1");
+    try (BasicDataSource dataSource =
+        (BasicDataSource) DataSourceUtils.createDataSource(properties)) {
+      Assertions.assertEquals(1, dataSource.getMaxIdle());
+      Assertions.assertEquals(1, dataSource.getMinIdle());
+    }
+  }
+
+  @Test
+  public void testRetainsConnectionsAcrossReadBursts() throws SQLException {
+    HashMap<String, String> properties = Maps.newHashMap();
+    properties.put(JdbcConfig.JDBC_DRIVER.getKey(), "org.sqlite.JDBC");
+    properties.put(JdbcConfig.JDBC_URL.getKey(), "jdbc:sqlite::memory:");
+    properties.put(JdbcConfig.USERNAME.getKey(), "test");
+    properties.put(JdbcConfig.PASSWORD.getKey(), "test");
+    properties.put(JdbcConfig.POOL_MAX_SIZE.getKey(), "16");
+    properties.put(JdbcConfig.POOL_MAX_IDLE.getKey(), "12");
+
+    try (BasicDataSource dataSource =
+        (BasicDataSource) DataSourceUtils.createDataSource(properties)) {
+      dataSource.setAccessToUnderlyingConnectionAllowed(true);
+      Set<Connection> physicalConnections = Collections.newSetFromMap(new 
IdentityHashMap<>());
+      for (int round = 0; round < 5; round++) {
+        List<Connection> borrowed = new ArrayList<>();
+        try {
+          for (int i = 0; i < 12; i++) {
+            Connection connection = dataSource.getConnection();
+            borrowed.add(connection);
+            physicalConnections.add(((DelegatingConnection<?>) 
connection).getInnermostDelegate());
+            try (Statement statement = connection.createStatement()) {
+              statement.execute("SELECT 1");
+            }
+          }
+        } finally {
+          for (Connection connection : borrowed) {
+            connection.close();
+          }
+        }
+        Assertions.assertEquals(12, dataSource.getNumIdle());
+      }
+      Assertions.assertFalse(physicalConnections.contains(null));
+      Assertions.assertEquals(12, physicalConnections.size());
+    }
+  }
+
   @Test
   public void testCloseDoesNotThrow() {
     JdbcCatalogOperations catalogOperations =
diff --git 
a/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/TestJdbcCatalogPropertiesMetadata.java
 
b/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/TestJdbcCatalogPropertiesMetadata.java
index 84099abd0c..1974134071 100644
--- 
a/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/TestJdbcCatalogPropertiesMetadata.java
+++ 
b/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/TestJdbcCatalogPropertiesMetadata.java
@@ -45,6 +45,7 @@ public class TestJdbcCatalogPropertiesMetadata {
 
     
Assertions.assertFalse(propertiesMetadata.isHiddenProperty(JdbcConfig.POOL_MIN_SIZE.getKey()));
     
Assertions.assertFalse(propertiesMetadata.isHiddenProperty(JdbcConfig.POOL_MAX_SIZE.getKey()));
+    
Assertions.assertFalse(propertiesMetadata.isHiddenProperty(JdbcConfig.POOL_MAX_IDLE.getKey()));
   }
 
   @Test
@@ -57,6 +58,7 @@ public class TestJdbcCatalogPropertiesMetadata {
             .put(JdbcConfig.PASSWORD.getKey(), "password")
             .put(JdbcConfig.POOL_MIN_SIZE.getKey(), "5")
             .put(JdbcConfig.POOL_MAX_SIZE.getKey(), "20")
+            .put(JdbcConfig.POOL_MAX_IDLE.getKey(), "16")
             .build();
 
     CatalogEntity catalogEntity =
@@ -80,6 +82,12 @@ public class TestJdbcCatalogPropertiesMetadata {
     Map<String, String> loadedProperties = loadCatalog.properties();
     Assertions.assertEquals("5", 
loadedProperties.get(JdbcConfig.POOL_MIN_SIZE.getKey()));
     Assertions.assertEquals("20", 
loadedProperties.get(JdbcConfig.POOL_MAX_SIZE.getKey()));
+    Assertions.assertEquals("16", 
loadedProperties.get(JdbcConfig.POOL_MAX_IDLE.getKey()));
+    Assertions.assertEquals(
+        "16",
+        new JdbcCatalogPropertiesMetadata()
+            .transformProperties(loadedProperties)
+            .get(JdbcConfig.POOL_MAX_IDLE.getKey()));
   }
 
   private static class TestJdbcCatalog extends JdbcCatalog {
diff --git 
a/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/config/TestJdbcConfig.java
 
b/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/config/TestJdbcConfig.java
index 194f82a76b..cfb19f0029 100644
--- 
a/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/config/TestJdbcConfig.java
+++ 
b/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/config/TestJdbcConfig.java
@@ -31,4 +31,21 @@ public class TestJdbcConfig {
     properties.put(JdbcConfig.JDBC_URL.getKey(), "jdbc:sqlite::memory:");
     Assertions.assertDoesNotThrow(() -> new JdbcConfig(properties));
   }
+
+  @Test
+  public void testMaxIdleDefaultsAndCap() {
+    JdbcConfig defaultConfig = new JdbcConfig(Maps.newHashMap());
+    Assertions.assertEquals(8, defaultConfig.getPoolMaxIdle());
+
+    HashMap<String, String> properties = Maps.newHashMap();
+    properties.put(JdbcConfig.POOL_MAX_SIZE.getKey(), "6");
+    Assertions.assertEquals(6, new JdbcConfig(properties).getPoolMaxIdle());
+
+    properties.put(JdbcConfig.POOL_MAX_IDLE.getKey(), "4");
+    Assertions.assertEquals(4, new JdbcConfig(properties).getPoolMaxIdle());
+
+    properties.put(JdbcConfig.POOL_MAX_IDLE.getKey(), "0");
+    Assertions.assertThrows(
+        IllegalArgumentException.class, () -> new 
JdbcConfig(properties).getPoolMaxIdle());
+  }
 }
diff --git 
a/catalogs/catalog-kafka/src/test/java/org/apache/gravitino/catalog/kafka/TestKafkaCatalogOperations.java
 
b/catalogs/catalog-kafka/src/test/java/org/apache/gravitino/catalog/kafka/TestKafkaCatalogOperations.java
index 0e0d9df1dc..a974515386 100644
--- 
a/catalogs/catalog-kafka/src/test/java/org/apache/gravitino/catalog/kafka/TestKafkaCatalogOperations.java
+++ 
b/catalogs/catalog-kafka/src/test/java/org/apache/gravitino/catalog/kafka/TestKafkaCatalogOperations.java
@@ -25,6 +25,7 @@ import static 
org.apache.gravitino.Configs.ENTITY_CHANGE_LOG_POLL_INTERVAL_SECS;
 import static org.apache.gravitino.Configs.ENTITY_CHANGE_LOG_RETENTION_SECS;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS;
+import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_PASSWORD;
 import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_PATH;
 import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL;
@@ -156,6 +157,7 @@ public class TestKafkaCatalogOperations extends 
KafkaClusterEmbedded {
     
when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_PASSWORD)).thenReturn("gravitino");
     
when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER)).thenReturn("org.h2.Driver");
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS)).thenReturn(100);
+    
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS)).thenReturn(10);
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS)).thenReturn(1000L);
 
     File f = FileUtils.getFile(STORE_PATH);
diff --git 
a/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/generic/TestGenericCatalogOperations.java
 
b/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/generic/TestGenericCatalogOperations.java
index 9af6e3c189..6bed860b0b 100644
--- 
a/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/generic/TestGenericCatalogOperations.java
+++ 
b/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/generic/TestGenericCatalogOperations.java
@@ -24,6 +24,7 @@ import static 
org.apache.gravitino.Configs.ENTITY_CHANGE_LOG_POLL_INTERVAL_SECS;
 import static org.apache.gravitino.Configs.ENTITY_CHANGE_LOG_RETENTION_SECS;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS;
+import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_PASSWORD;
 import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_PATH;
 import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL;
@@ -115,6 +116,7 @@ public class TestGenericCatalogOperations {
     
when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_PASSWORD)).thenReturn("gravitino");
     
when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER)).thenReturn("org.h2.Driver");
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS)).thenReturn(100);
+    
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS)).thenReturn(10);
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS)).thenReturn(1000L);
 
     File f = FileUtils.getFile(STORE_PATH);
diff --git 
a/catalogs/catalog-model/src/test/java/org/apache/gravtitino/catalog/model/TestModelCatalogOperations.java
 
b/catalogs/catalog-model/src/test/java/org/apache/gravtitino/catalog/model/TestModelCatalogOperations.java
index 34fcc5c900..4ef7a9f3be 100644
--- 
a/catalogs/catalog-model/src/test/java/org/apache/gravtitino/catalog/model/TestModelCatalogOperations.java
+++ 
b/catalogs/catalog-model/src/test/java/org/apache/gravtitino/catalog/model/TestModelCatalogOperations.java
@@ -24,6 +24,7 @@ import static 
org.apache.gravitino.Configs.ENTITY_CHANGE_LOG_POLL_INTERVAL_SECS;
 import static org.apache.gravitino.Configs.ENTITY_CHANGE_LOG_RETENTION_SECS;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS;
+import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_PASSWORD;
 import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_PATH;
 import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL;
@@ -121,6 +122,7 @@ public class TestModelCatalogOperations {
     
when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_PASSWORD)).thenReturn("gravitino");
     
when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER)).thenReturn("org.h2.Driver");
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS)).thenReturn(100);
+    
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS)).thenReturn(10);
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS)).thenReturn(1000L);
 
     when(config.get(VERSION_RETENTION_COUNT)).thenReturn(1L);
diff --git a/conf/gravitino.conf.template b/conf/gravitino.conf.template
index f5732377fc..af8a6c0651 100644
--- a/conf/gravitino.conf.template
+++ b/conf/gravitino.conf.template
@@ -70,6 +70,9 @@ gravitino.entity.store.relational.jdbcDriver = org.h2.Driver
 gravitino.entity.store.relational.jdbcUser = gravitino
 # The JDBC password
 gravitino.entity.store.relational.jdbcPassword = gravitino
+# Maximum idle entity-store connections retained by this server (capped by 
maxConnections).
+# Budget for it on every server, plus any JDBC catalog pools that use the same 
database instance.
+# gravitino.entity.store.relational.maxIdleConnections = 10
 
 # THE CONFIGURATION FOR Gravitino CATALOG
 # The interval in milliseconds to evict the catalog cache
diff --git 
a/core/src/jmh/java/org/apache/gravitino/cache/it/AbstractEntityStorageBenchmark.java
 
b/core/src/jmh/java/org/apache/gravitino/cache/it/AbstractEntityStorageBenchmark.java
index ba733f5f16..16e8eed9c5 100644
--- 
a/core/src/jmh/java/org/apache/gravitino/cache/it/AbstractEntityStorageBenchmark.java
+++ 
b/core/src/jmh/java/org/apache/gravitino/cache/it/AbstractEntityStorageBenchmark.java
@@ -20,11 +20,13 @@
 package org.apache.gravitino.cache.it;
 
 import static org.apache.gravitino.Configs.DEFAULT_ENTITY_RELATIONAL_STORE;
+import static 
org.apache.gravitino.Configs.DEFAULT_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS;
 import static 
org.apache.gravitino.Configs.ENTITY_CHANGE_LOG_CLEANUP_INTERVAL_SECS;
 import static 
org.apache.gravitino.Configs.ENTITY_CHANGE_LOG_POLL_INTERVAL_SECS;
 import static org.apache.gravitino.Configs.ENTITY_CHANGE_LOG_RETENTION_SECS;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS;
+import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_PASSWORD;
 import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_PATH;
 import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL;
@@ -291,6 +293,8 @@ public class AbstractEntityStorageBenchmark<E extends 
Entity & HasIdentifier> {
     
Mockito.when(config.get(ENTITY_RELATIONAL_STORE)).thenReturn(DEFAULT_ENTITY_RELATIONAL_STORE);
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_PATH)).thenReturn(DB_DIR);
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS)).thenReturn(100);
+    
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS))
+        .thenReturn(DEFAULT_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS);
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS)).thenReturn(1000L);
     Mockito.when(config.get(STORE_DELETE_AFTER_TIME)).thenReturn(20 * 60 * 
1000L);
     
Mockito.when(config.get(ENTITY_CHANGE_LOG_POLL_INTERVAL_SECS)).thenReturn(3L);
diff --git a/core/src/main/java/org/apache/gravitino/Configs.java 
b/core/src/main/java/org/apache/gravitino/Configs.java
index 5c016854cf..108f316219 100644
--- a/core/src/main/java/org/apache/gravitino/Configs.java
+++ b/core/src/main/java/org/apache/gravitino/Configs.java
@@ -55,6 +55,10 @@ public class Configs {
   public static final String 
ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTION_KEYS =
       "gravitino.entity.store.relational.maxConnections";
 
+  /** Configuration key for the entity-store pool's maximum idle connections. 
*/
+  public static final String 
ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS_KEY =
+      "gravitino.entity.store.relational.maxIdleConnections";
+
   public static final String 
ENTITY_RELATIONAL_JDBC_BACKEND_MAX_WAIT_MILLIS_CONNECTION_KEY =
       "gravitino.entity.store.relational.maxWaitMillis";
 
@@ -96,6 +100,9 @@ public class Configs {
 
   public static final int DEFAULT_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS = 
100;
 
+  /** Default maximum idle connections retained by the entity-store pool on 
each server. */
+  public static final int DEFAULT_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS 
= 10;
+
   public static final int DEFAULT_GRAVITINO_AUTHORIZATION_THREAD_POOL_SIZE = 
100;
 
   public static final int DEFAULT_BULK_MAX_ITEMS = 100;
@@ -162,6 +169,15 @@ public class Configs {
           .intConf()
           .createWithDefault(DEFAULT_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS);
 
+  /** Maximum idle connections retained by the entity-store pool, capped by 
maxConnections. */
+  public static final ConfigEntry<Integer> 
ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS =
+      new 
ConfigBuilder(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS_KEY)
+          .doc("The maximum number of idle connections retained by the JDBC 
Backend pool")
+          .version(ConfigConstants.VERSION_2_0_0)
+          .intConf()
+          .checkValue(value -> value > 0, 
ConfigConstants.POSITIVE_NUMBER_ERROR_MSG)
+          
.createWithDefault(DEFAULT_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS);
+
   public static final ConfigEntry<Long> 
ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS =
       new 
ConfigBuilder(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_WAIT_MILLIS_CONNECTION_KEY)
           .doc(
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/session/SqlSessionFactoryHelper.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/session/SqlSessionFactoryHelper.java
index 05c92c0769..2b52ec1964 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/session/SqlSessionFactoryHelper.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/session/SqlSessionFactoryHelper.java
@@ -48,7 +48,6 @@ import 
org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
  * should be initialized only once.
  */
 public class SqlSessionFactoryHelper {
-  private static final int JDBC_BACKEND_MAX_IDLE_CONNECTIONS = 10;
   private static final int JDBC_BACKEND_MIN_IDLE_CONNECTIONS = 5;
   private static final Duration JDBC_BACKEND_MIN_EVICTABLE_IDLE_TIME = 
Duration.ofSeconds(30);
 
@@ -93,9 +92,14 @@ public class SqlSessionFactoryHelper {
       dataSource.setDefaultAutoCommit(false);
       dataSource.setMaxWaitMillis(
           
config.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS));
-      
dataSource.setMaxTotal(config.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS));
-      dataSource.setMaxIdle(JDBC_BACKEND_MAX_IDLE_CONNECTIONS);
-      dataSource.setMinIdle(JDBC_BACKEND_MIN_IDLE_CONNECTIONS);
+      int maxConnections = 
config.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS);
+      int configuredMaxIdle =
+          
config.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS);
+      int maxIdle =
+          maxConnections > 0 ? Math.min(maxConnections, configuredMaxIdle) : 
configuredMaxIdle;
+      dataSource.setMaxTotal(maxConnections);
+      dataSource.setMaxIdle(maxIdle);
+      dataSource.setMinIdle(Math.min(JDBC_BACKEND_MIN_IDLE_CONNECTIONS, 
maxIdle));
       dataSource.setLogAbandoned(true);
       dataSource.setRemoveAbandonedOnBorrow(true);
       dataSource.setRemoveAbandonedTimeout(60);
diff --git a/core/src/test/java/org/apache/gravitino/TestConfig.java 
b/core/src/test/java/org/apache/gravitino/TestConfig.java
index 560e7f05d3..f7c03df4b7 100644
--- a/core/src/test/java/org/apache/gravitino/TestConfig.java
+++ b/core/src/test/java/org/apache/gravitino/TestConfig.java
@@ -173,4 +173,18 @@ public class TestConfig {
     ConfigEntry<Boolean> boolEntry = new 
ConfigBuilder("boolean.key").booleanConf();
     Assertions.assertTrue(boolEntry.readFrom(props));
   }
+
+  @Test
+  public void testEntityStoreMaxIdleConnections() {
+    Config config = new Config(false) {};
+    Assertions.assertEquals(
+        10, 
config.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS));
+
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () -> {
+          
config.set(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS, 0);
+          
config.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS);
+        });
+  }
 }
diff --git 
a/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java
 
b/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java
index ac0089fb80..01d92dd44d 100644
--- 
a/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java
+++ 
b/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java
@@ -25,6 +25,7 @@ import static 
org.apache.gravitino.Configs.ENTITY_CHANGE_LOG_POLL_INTERVAL_SECS;
 import static org.apache.gravitino.Configs.ENTITY_CHANGE_LOG_RETENTION_SECS;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS;
+import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS;
 import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS;
 import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_STORE;
@@ -150,6 +151,7 @@ public class TestAccessControlManager {
         
.thenReturn(String.format("jdbc:h2:file:%s;DB_CLOSE_DELAY=-1;MODE=MYSQL", 
DB_DIR));
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER)).thenReturn("org.h2.Driver");
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS)).thenReturn(100);
+    
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS)).thenReturn(10);
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS)).thenReturn(1000L);
     
Mockito.when(config.get(STORE_TRANSACTION_MAX_SKEW_TIME)).thenReturn(1000L);
     Mockito.when(config.get(STORE_DELETE_AFTER_TIME)).thenReturn(20 * 60 * 
1000L);
diff --git 
a/core/src/test/java/org/apache/gravitino/authorization/TestOwnerManager.java 
b/core/src/test/java/org/apache/gravitino/authorization/TestOwnerManager.java
index 832b00c993..e17d9b6f10 100644
--- 
a/core/src/test/java/org/apache/gravitino/authorization/TestOwnerManager.java
+++ 
b/core/src/test/java/org/apache/gravitino/authorization/TestOwnerManager.java
@@ -25,6 +25,7 @@ import static 
org.apache.gravitino.Configs.ENTITY_CHANGE_LOG_POLL_INTERVAL_SECS;
 import static org.apache.gravitino.Configs.ENTITY_CHANGE_LOG_RETENTION_SECS;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS;
+import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS;
 import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS;
 import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_STORE;
@@ -110,6 +111,7 @@ public class TestOwnerManager {
         
.thenReturn(String.format("jdbc:h2:file:%s;DB_CLOSE_DELAY=-1;MODE=MYSQL", 
DB_DIR));
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER)).thenReturn("org.h2.Driver");
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS)).thenReturn(100);
+    
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS)).thenReturn(10);
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS)).thenReturn(1000L);
     
Mockito.when(config.get(STORE_TRANSACTION_MAX_SKEW_TIME)).thenReturn(1000L);
     Mockito.when(config.get(STORE_DELETE_AFTER_TIME)).thenReturn(20 * 60 * 
1000L);
diff --git 
a/core/src/test/java/org/apache/gravitino/hook/TestFilesetHookDispatcher.java 
b/core/src/test/java/org/apache/gravitino/hook/TestFilesetHookDispatcher.java
index 1a47a83025..4037f5d2c7 100644
--- 
a/core/src/test/java/org/apache/gravitino/hook/TestFilesetHookDispatcher.java
+++ 
b/core/src/test/java/org/apache/gravitino/hook/TestFilesetHookDispatcher.java
@@ -25,6 +25,7 @@ import static 
org.apache.gravitino.Configs.ENTITY_CHANGE_LOG_POLL_INTERVAL_SECS;
 import static org.apache.gravitino.Configs.ENTITY_CHANGE_LOG_RETENTION_SECS;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS;
+import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS;
 import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS;
 import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_STORE;
@@ -240,6 +241,8 @@ public class TestFilesetHookDispatcher extends 
TestOperationDispatcher {
           Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER))
               .thenReturn("org.h2.Driver");
           
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS)).thenReturn(100);
+          
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS))
+              .thenReturn(10);
           
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS))
               .thenReturn(1000L);
           
Mockito.when(config.get(STORE_TRANSACTION_MAX_SKEW_TIME)).thenReturn(1000L);
diff --git 
a/core/src/test/java/org/apache/gravitino/policy/TestPolicyManager.java 
b/core/src/test/java/org/apache/gravitino/policy/TestPolicyManager.java
index e588b24374..3ec76afae6 100644
--- a/core/src/test/java/org/apache/gravitino/policy/TestPolicyManager.java
+++ b/core/src/test/java/org/apache/gravitino/policy/TestPolicyManager.java
@@ -25,6 +25,7 @@ import static 
org.apache.gravitino.Configs.ENTITY_CHANGE_LOG_POLL_INTERVAL_SECS;
 import static org.apache.gravitino.Configs.ENTITY_CHANGE_LOG_RETENTION_SECS;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS;
+import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS;
 import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS;
 import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_STORE;
@@ -242,6 +243,7 @@ public class TestPolicyManager {
         
.thenReturn(String.format("jdbc:h2:file:%s;DB_CLOSE_DELAY=-1;MODE=MYSQL", 
DB_DIR));
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER)).thenReturn("org.h2.Driver");
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS)).thenReturn(100);
+    
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS)).thenReturn(10);
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS)).thenReturn(1000L);
     
Mockito.when(config.get(STORE_TRANSACTION_MAX_SKEW_TIME)).thenReturn(1000L);
     Mockito.when(config.get(STORE_DELETE_AFTER_TIME)).thenReturn(20 * 60 * 
1000L);
diff --git 
a/core/src/test/java/org/apache/gravitino/stats/TestStatisticManager.java 
b/core/src/test/java/org/apache/gravitino/stats/TestStatisticManager.java
index aa0cb0fea5..46e82715d8 100644
--- a/core/src/test/java/org/apache/gravitino/stats/TestStatisticManager.java
+++ b/core/src/test/java/org/apache/gravitino/stats/TestStatisticManager.java
@@ -25,6 +25,7 @@ import static 
org.apache.gravitino.Configs.ENTITY_CHANGE_LOG_POLL_INTERVAL_SECS;
 import static org.apache.gravitino.Configs.ENTITY_CHANGE_LOG_RETENTION_SECS;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS;
+import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS;
 import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS;
 import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_STORE;
@@ -109,6 +110,7 @@ public class TestStatisticManager {
         
.thenReturn(String.format("jdbc:h2:file:%s;DB_CLOSE_DELAY=-1;MODE=MYSQL", 
DB_DIR));
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER)).thenReturn("org.h2.Driver");
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS)).thenReturn(100);
+    
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS)).thenReturn(10);
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS)).thenReturn(1000L);
     
Mockito.when(config.get(STORE_TRANSACTION_MAX_SKEW_TIME)).thenReturn(1000L);
     Mockito.when(config.get(STORE_DELETE_AFTER_TIME)).thenReturn(20 * 60 * 
1000L);
diff --git 
a/core/src/test/java/org/apache/gravitino/storage/AbstractEntityStorageTest.java
 
b/core/src/test/java/org/apache/gravitino/storage/AbstractEntityStorageTest.java
index cac53eed61..7c8142d4b7 100644
--- 
a/core/src/test/java/org/apache/gravitino/storage/AbstractEntityStorageTest.java
+++ 
b/core/src/test/java/org/apache/gravitino/storage/AbstractEntityStorageTest.java
@@ -25,6 +25,7 @@ import static 
org.apache.gravitino.Configs.ENTITY_CHANGE_LOG_POLL_INTERVAL_SECS;
 import static org.apache.gravitino.Configs.ENTITY_CHANGE_LOG_RETENTION_SECS;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS;
+import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_PASSWORD;
 import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_PATH;
 import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL;
@@ -144,6 +145,7 @@ abstract class AbstractEntityStorageTest {
     
Mockito.when(config.get(ENTITY_RELATIONAL_STORE)).thenReturn(DEFAULT_ENTITY_RELATIONAL_STORE);
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_PATH)).thenReturn(DB_DIR);
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS)).thenReturn(100);
+    
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS)).thenReturn(10);
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS)).thenReturn(1000L);
     Mockito.when(config.get(STORE_DELETE_AFTER_TIME)).thenReturn(20 * 60 * 
1000L);
     
Mockito.when(config.get(ENTITY_CHANGE_LOG_POLL_INTERVAL_SECS)).thenReturn(3L);
diff --git 
a/core/src/test/java/org/apache/gravitino/storage/relational/BackendTestExtension.java
 
b/core/src/test/java/org/apache/gravitino/storage/relational/BackendTestExtension.java
index a9377c8b59..9961e30bb1 100644
--- 
a/core/src/test/java/org/apache/gravitino/storage/relational/BackendTestExtension.java
+++ 
b/core/src/test/java/org/apache/gravitino/storage/relational/BackendTestExtension.java
@@ -25,6 +25,7 @@ import static 
org.apache.gravitino.Configs.DEFAULT_RELATIONAL_JDBC_BACKEND_MAX_C
 import static 
org.apache.gravitino.Configs.DEFAULT_RELATIONAL_JDBC_BACKEND_MAX_WAIT_MILLISECONDS;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS;
+import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_PASSWORD;
 import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL;
 import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_USER;
@@ -173,6 +174,7 @@ public class BackendTestExtension
           .thenReturn(DEFAULT_ENTITY_RELATIONAL_STORE);
       Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS))
           .thenReturn(DEFAULT_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS);
+      
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS)).thenReturn(10);
       
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS))
           .thenReturn(DEFAULT_RELATIONAL_JDBC_BACKEND_MAX_WAIT_MILLISECONDS);
 
diff --git 
a/core/src/test/java/org/apache/gravitino/storage/relational/TestRelationalEntityStoreHierarchicalCache.java
 
b/core/src/test/java/org/apache/gravitino/storage/relational/TestRelationalEntityStoreHierarchicalCache.java
index 0ac25beede..9fa204a8d2 100644
--- 
a/core/src/test/java/org/apache/gravitino/storage/relational/TestRelationalEntityStoreHierarchicalCache.java
+++ 
b/core/src/test/java/org/apache/gravitino/storage/relational/TestRelationalEntityStoreHierarchicalCache.java
@@ -198,6 +198,8 @@ public class TestRelationalEntityStoreHierarchicalCache {
         .thenReturn("org.h2.Driver");
     
Mockito.when(config.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS))
         .thenReturn(Configs.DEFAULT_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS);
+    
Mockito.when(config.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS))
+        .thenReturn(10);
     
Mockito.when(config.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS))
         
.thenReturn(Configs.DEFAULT_RELATIONAL_JDBC_BACKEND_MAX_WAIT_MILLISECONDS);
     Mockito.when(config.get(Configs.STORE_DELETE_AFTER_TIME)).thenReturn(20 * 
60 * 1000L);
diff --git 
a/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestAuthMappers.java
 
b/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestAuthMappers.java
index 79995003a8..ac53847ef8 100644
--- 
a/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestAuthMappers.java
+++ 
b/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestAuthMappers.java
@@ -92,6 +92,8 @@ public class TestAuthMappers {
     Mockito.when(config.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER))
         .thenReturn("org.h2.Driver");
     
Mockito.when(config.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS)).thenReturn(20);
+    
Mockito.when(config.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS))
+        .thenReturn(10);
     
Mockito.when(config.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS))
         .thenReturn(1000L);
 
diff --git 
a/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestEntityChangeLogMapper.java
 
b/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestEntityChangeLogMapper.java
index 89fa8d6543..5e34cd9696 100644
--- 
a/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestEntityChangeLogMapper.java
+++ 
b/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestEntityChangeLogMapper.java
@@ -71,6 +71,8 @@ public class TestEntityChangeLogMapper {
     Mockito.when(config.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER))
         .thenReturn("org.h2.Driver");
     
Mockito.when(config.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS)).thenReturn(20);
+    
Mockito.when(config.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS))
+        .thenReturn(10);
     
Mockito.when(config.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS))
         .thenReturn(1000L);
 
diff --git 
a/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestUserRoleRelMapper.java
 
b/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestUserRoleRelMapper.java
index 38e9b044cc..6cebaa5a31 100644
--- 
a/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestUserRoleRelMapper.java
+++ 
b/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestUserRoleRelMapper.java
@@ -82,6 +82,8 @@ public class TestUserRoleRelMapper {
     Mockito.when(config.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER))
         .thenReturn("org.h2.Driver");
     
Mockito.when(config.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS)).thenReturn(20);
+    
Mockito.when(config.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS))
+        .thenReturn(10);
     
Mockito.when(config.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS))
         .thenReturn(1000L);
 
diff --git 
a/core/src/test/java/org/apache/gravitino/storage/relational/session/TestSqlSession.java
 
b/core/src/test/java/org/apache/gravitino/storage/relational/session/TestSqlSession.java
index fd93824d10..15e824de5a 100644
--- 
a/core/src/test/java/org/apache/gravitino/storage/relational/session/TestSqlSession.java
+++ 
b/core/src/test/java/org/apache/gravitino/storage/relational/session/TestSqlSession.java
@@ -22,6 +22,7 @@ package org.apache.gravitino.storage.relational.session;
 import static org.apache.gravitino.Configs.DEFAULT_ENTITY_RELATIONAL_STORE;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS;
+import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_PASSWORD;
 import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL;
 import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_USER;
@@ -78,6 +79,7 @@ public class TestSqlSession {
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_PASSWORD)).thenReturn("123");
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER)).thenReturn("org.h2.Driver");
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS)).thenReturn(100);
+    
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS)).thenReturn(10);
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS)).thenReturn(1000L);
   }
 
@@ -129,6 +131,68 @@ public class TestSqlSession {
     assertEquals(Duration.ofSeconds(30).toMillis(), 
dataSource.getMinEvictableIdleTimeMillis());
   }
 
+  @Test
+  public void testConfiguredMaxIdleAndPoolCap() {
+    SqlSessionFactoryHelper.getInstance().close();
+    
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS)).thenReturn(64);
+    
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS)).thenReturn(40);
+    try {
+      SqlSessionFactoryHelper.getInstance().init(config);
+      BasicDataSource dataSource =
+          (BasicDataSource)
+              SqlSessionFactoryHelper.getInstance()
+                  .getSqlSessionFactory()
+                  .getConfiguration()
+                  .getEnvironment()
+                  .getDataSource();
+      assertEquals(40, dataSource.getMaxTotal());
+      assertEquals(40, dataSource.getMaxIdle());
+    } finally {
+      
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS)).thenReturn(10);
+      
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS)).thenReturn(100);
+    }
+  }
+
+  @Test
+  public void testMinIdleCappedByMaxIdle() {
+    SqlSessionFactoryHelper.getInstance().close();
+    
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS)).thenReturn(3);
+    try {
+      SqlSessionFactoryHelper.getInstance().init(config);
+      BasicDataSource dataSource =
+          (BasicDataSource)
+              SqlSessionFactoryHelper.getInstance()
+                  .getSqlSessionFactory()
+                  .getConfiguration()
+                  .getEnvironment()
+                  .getDataSource();
+      assertEquals(3, dataSource.getMaxIdle());
+      assertEquals(3, dataSource.getMinIdle());
+    } finally {
+      
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS)).thenReturn(10);
+    }
+  }
+
+  @Test
+  public void testUnlimitedPoolStillLimitsIdleConnections() {
+    SqlSessionFactoryHelper.getInstance().close();
+    
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS)).thenReturn(-1);
+    try {
+      SqlSessionFactoryHelper.getInstance().init(config);
+      BasicDataSource dataSource =
+          (BasicDataSource)
+              SqlSessionFactoryHelper.getInstance()
+                  .getSqlSessionFactory()
+                  .getConfiguration()
+                  .getEnvironment()
+                  .getDataSource();
+      assertEquals(-1, dataSource.getMaxTotal());
+      assertEquals(10, dataSource.getMaxIdle());
+    } finally {
+      
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS)).thenReturn(100);
+    }
+  }
+
   @Test
   public void testGetSqlSessionFactoryWithoutInit() {
     SqlSessionFactoryHelper.getInstance().close();
diff --git a/core/src/test/java/org/apache/gravitino/tag/TestTagManager.java 
b/core/src/test/java/org/apache/gravitino/tag/TestTagManager.java
index 74156b1260..de3f9c8473 100644
--- a/core/src/test/java/org/apache/gravitino/tag/TestTagManager.java
+++ b/core/src/test/java/org/apache/gravitino/tag/TestTagManager.java
@@ -24,6 +24,7 @@ import static 
org.apache.gravitino.Configs.ENTITY_CHANGE_LOG_POLL_INTERVAL_SECS;
 import static org.apache.gravitino.Configs.ENTITY_CHANGE_LOG_RETENTION_SECS;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS;
+import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS;
 import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS;
 import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_STORE;
@@ -167,6 +168,7 @@ public class TestTagManager {
         
.thenReturn(String.format("jdbc:h2:file:%s;DB_CLOSE_DELAY=-1;MODE=MYSQL", 
DB_DIR));
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER)).thenReturn("org.h2.Driver");
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS)).thenReturn(100);
+    
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_MAX_IDLE_CONNECTIONS)).thenReturn(10);
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS)).thenReturn(1000L);
     
Mockito.when(config.get(STORE_TRANSACTION_MAX_SKEW_TIME)).thenReturn(1000L);
     Mockito.when(config.get(STORE_DELETE_AFTER_TIME)).thenReturn(20 * 60 * 
1000L);
diff --git a/dev/charts/gravitino/resources/config/gravitino.conf 
b/dev/charts/gravitino/resources/config/gravitino.conf
index b715195a70..d4c9f16a4c 100644
--- a/dev/charts/gravitino/resources/config/gravitino.conf
+++ b/dev/charts/gravitino/resources/config/gravitino.conf
@@ -51,6 +51,9 @@ gravitino.entity.store.relational.storagePath = {{ 
.Values.entity.storagePath }}
 {{- if .Values.entity.maxConnections }}
 gravitino.entity.store.relational.maxConnections = {{ 
.Values.entity.maxConnections }}
 {{- end }}
+{{- if .Values.entity.maxIdleConnections }}
+gravitino.entity.store.relational.maxIdleConnections = {{ 
.Values.entity.maxIdleConnections }}
+{{- end }}
 
 {{- if or .Values.lock.maxNodes .Values.lock.minNodes }}
 
diff --git a/dev/charts/gravitino/tests/configmap_test.yaml 
b/dev/charts/gravitino/tests/configmap_test.yaml
index 761c007847..ef05708b05 100644
--- a/dev/charts/gravitino/tests/configmap_test.yaml
+++ b/dev/charts/gravitino/tests/configmap_test.yaml
@@ -275,6 +275,25 @@ tests:
           path: data["gravitino.conf"]
           pattern: "gravitino\\.entity\\.store\\.relational\\.maxConnections"
 
+  - it: renders entity.maxIdleConnections only when set
+    release:
+      name: gravitino
+    set:
+      entity:
+        maxIdleConnections: 64
+    asserts:
+      - matchRegex:
+          path: data["gravitino.conf"]
+          pattern: 
"gravitino\\.entity\\.store\\.relational\\.maxIdleConnections = 64"
+
+  - it: does not render entity.maxIdleConnections when empty (default)
+    release:
+      name: gravitino
+    asserts:
+      - notMatchRegex:
+          path: data["gravitino.conf"]
+          pattern: 
"gravitino\\.entity\\.store\\.relational\\.maxIdleConnections"
+
   - it: renders lock.maxNodes and lock.minNodes only when set
     release:
       name: gravitino
diff --git a/dev/charts/gravitino/values.yaml b/dev/charts/gravitino/values.yaml
index b18b043a87..022b61e97a 100644
--- a/dev/charts/gravitino/values.yaml
+++ b/dev/charts/gravitino/values.yaml
@@ -144,6 +144,11 @@ entity:
   ## Leave unset to use the server default. Production recommendation: 200.
   ##
   maxConnections: ""
+  ## Maximum idle JDBC connections the relational backend pool keeps on each 
server, capped by
+  ## maxConnections. Leave unset to use the server default (10). Budget for 
this value on every
+  ## replica, because idle connections are released slowly.
+  ##
+  maxIdleConnections: ""
 
 ## THE CONFIGURATION FOR Gravitino TREE LOCK
 ##
diff --git a/dev/performance/jdbc_idle_retention.py 
b/dev/performance/jdbc_idle_retention.py
new file mode 100644
index 0000000000..9859b2a35c
--- /dev/null
+++ b/dev/performance/jdbc_idle_retention.py
@@ -0,0 +1,195 @@
+#!/usr/bin/env python3
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#  http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Measure JDBC connection churn and HTTP tail latency under concurrent reads.
+
+Run the same workload against fresh server processes with only the idle limit 
changed, for
+example::
+
+    python3 dev/performance/jdbc_idle_retention.py \\
+      --url http://localhost:8090/api/metalakes \\
+      --mysql-defaults-extra-file=/path/to/mysql-client.cnf \\
+      --clients 64 --warmup-seconds 3 --seconds 15 --rounds 3
+
+The MySQL client option file supplies the database credentials. Set
+GRAVITINO_BENCH_AUTHORIZATION to send an Authorization header. Use a catalog 
read URL, such as
+the list-tables route, to test a JDBC catalog pool instead of the entity-store 
pool.
+
+Each round prints successful throughput, p50/p99 latency, failures, and the 
change in MySQL's
+server-wide Connections and Threads_created counters. Those counters include 
this script's own
+status queries and any other database traffic, so compare runs on an otherwise 
quiet database with
+the same client count, data, JVM settings and node count, and only after 
confirming that no
+requests failed.
+
+Self-test::
+
+    cd dev/performance && PYTHONDONTWRITEBYTECODE=1 python3 -m unittest 
test_jdbc_idle_retention
+"""
+
+import argparse
+import concurrent.futures
+import http.client
+import json
+import math
+import os
+import subprocess
+import sys
+import time
+from urllib.parse import urlsplit
+
+
+def percentile(samples, fraction):
+    """Return a nearest-rank percentile in milliseconds."""
+    if not samples:
+        return None
+    ordered = sorted(samples)
+    return ordered[math.ceil(fraction * len(ordered)) - 1]
+
+
+def parse_mysql_status(output):
+    """Parse MySQL's two-column, tab-separated status output."""
+    return {
+        name: int(value)
+        for name, value in (line.split("\t") for line in output.splitlines())
+    }
+
+
+def mysql_status(defaults_file):
+    """Read server-wide connection counters using credentials from a MySQL 
option file."""
+    result = subprocess.run(
+        [
+            "mysql",
+            f"--defaults-extra-file={defaults_file}",
+            "--batch",
+            "--skip-column-names",
+            "-e",
+            "SHOW GLOBAL STATUS WHERE Variable_name IN ('Connections', 
'Threads_created')",
+        ],
+        check=True,
+        capture_output=True,
+        text=True,
+    )
+    return parse_mysql_status(result.stdout)
+
+
+def run_phase(url, clients, duration, authorization):
+    """Send keep-alive GET requests from each client for one timed phase."""
+    parsed = urlsplit(url)
+    connection_type = (
+        http.client.HTTPSConnection
+        if parsed.scheme == "https"
+        else http.client.HTTPConnection
+    )
+    path = parsed.path or "/"
+    if parsed.query:
+        path += "?" + parsed.query
+    headers = {"Authorization": authorization} if authorization else {}
+    deadline = time.monotonic() + duration
+
+    def worker():
+        latencies = []
+        errors = 0
+        connection = None
+        try:
+            while time.monotonic() < deadline:
+                if connection is None:
+                    connection = connection_type(
+                        parsed.hostname, parsed.port, timeout=10
+                    )
+                start = time.perf_counter()
+                try:
+                    connection.request("GET", path, headers=headers)
+                    response = connection.getresponse()
+                    response.read()
+                    elapsed_ms = (time.perf_counter() - start) * 1000
+                    if 200 <= response.status < 300:
+                        latencies.append(elapsed_ms)
+                    else:
+                        errors += 1
+                except (OSError, http.client.HTTPException):
+                    errors += 1
+                    connection.close()
+                    connection = None
+        finally:
+            if connection is not None:
+                connection.close()
+        return latencies, errors
+
+    start = time.monotonic()
+    with concurrent.futures.ThreadPoolExecutor(max_workers=clients) as 
executor:
+        results = list(executor.map(lambda _: worker(), range(clients)))
+    elapsed = time.monotonic() - start
+    latencies = [
+        latency for client_latencies, _ in results for latency in 
client_latencies
+    ]
+    errors = sum(client_errors for _, client_errors in results)
+    return {
+        "requests": len(latencies),
+        "errors": errors,
+        "throughput_per_second": round(len(latencies) / elapsed, 1),
+        "p50_ms": percentile(latencies, 0.50),
+        "p99_ms": percentile(latencies, 0.99),
+    }
+
+
+def main():
+    """Run warmup and measured rounds against an already-started Gravitino 
server."""
+    parser = argparse.ArgumentParser(
+        description=__doc__, 
formatter_class=argparse.RawDescriptionHelpFormatter
+    )
+    parser.add_argument(
+        "--url",
+        required=True,
+        help="full URL of a GET route, such as 
http://localhost:8090/api/metalakes";,
+    )
+    parser.add_argument("--mysql-defaults-extra-file", required=True)
+    parser.add_argument("--clients", type=int, default=64)
+    parser.add_argument("--warmup-seconds", type=float, default=3)
+    parser.add_argument("--seconds", type=float, default=15)
+    parser.add_argument("--rounds", type=int, default=3)
+    args = parser.parse_args()
+    if (
+        args.clients < 1
+        or args.warmup_seconds <= 0
+        or args.seconds <= 0
+        or args.rounds < 1
+    ):
+        parser.error("clients, warmup-seconds, seconds, and rounds must be 
positive")
+    if urlsplit(args.url).scheme not in ("http", "https"):
+        parser.error("url must use http or https")
+
+    authorization = os.environ.get("GRAVITINO_BENCH_AUTHORIZATION")
+    had_errors = False
+    for round_number in range(1, args.rounds + 1):
+        run_phase(args.url, args.clients, args.warmup_seconds, authorization)
+        before = mysql_status(args.mysql_defaults_extra_file)
+        result = run_phase(args.url, args.clients, args.seconds, authorization)
+        after = mysql_status(args.mysql_defaults_extra_file)
+        result["round"] = round_number
+        had_errors |= result["errors"] > 0
+        result["mysql_connections_delta"] = after["Connections"] - 
before["Connections"]
+        result["mysql_threads_created_delta"] = (
+            after["Threads_created"] - before["Threads_created"]
+        )
+        print(json.dumps(result, sort_keys=True), flush=True)
+    if had_errors:
+        sys.exit(1)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/dev/performance/test_jdbc_idle_retention.py 
b/dev/performance/test_jdbc_idle_retention.py
new file mode 100644
index 0000000000..abd8c72ba9
--- /dev/null
+++ b/dev/performance/test_jdbc_idle_retention.py
@@ -0,0 +1,77 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#  http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import unittest
+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+from threading import Thread
+from unittest.mock import patch
+
+from jdbc_idle_retention import mysql_status, parse_mysql_status, percentile, 
run_phase
+
+
+class TestJdbcIdleRetention(unittest.TestCase):
+    def test_percentile(self):
+        self.assertIsNone(percentile([], 0.99))
+        self.assertEqual(1, percentile([4, 1, 3, 2], 0.25))
+        self.assertEqual(4, percentile([4, 1, 3, 2], 0.99))
+
+    def test_parse_mysql_status(self):
+        self.assertEqual(
+            {"Connections": 37, "Threads_created": 4},
+            parse_mysql_status("Connections\t37\nThreads_created\t4\n"),
+        )
+
+    @patch("jdbc_idle_retention.subprocess.run")
+    def test_mysql_status(self, run):
+        run.return_value.stdout = "Connections\t37\nThreads_created\t4\n"
+        self.assertEqual(
+            {"Connections": 37, "Threads_created": 4}, 
mysql_status("/tmp/mysql.cnf")
+        )
+        self.assertEqual(
+            "--defaults-extra-file=/tmp/mysql.cnf", run.call_args.args[0][1]
+        )
+
+    def test_run_phase(self):
+        class Handler(BaseHTTPRequestHandler):
+            protocol_version = "HTTP/1.1"
+
+            def do_GET(self):
+                self.send_response(200)
+                self.send_header("Content-Length", "0")
+                self.end_headers()
+
+            def log_message(self, *_args):
+                pass
+
+        server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
+        thread = Thread(target=server.serve_forever, daemon=True)
+        thread.start()
+        try:
+            result = run_phase(
+                f"http://127.0.0.1:{server.server_port}/api/metalakes";, 4, 
0.5, None
+            )
+            self.assertGreater(result["requests"], 0)
+            self.assertEqual(0, result["errors"])
+            self.assertGreaterEqual(result["p99_ms"], 0)
+        finally:
+            server.shutdown()
+            server.server_close()
+            thread.join()
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/docs/gravitino-server-config.md b/docs/gravitino-server-config.md
index 965e1a71c8..672dd01340 100644
--- a/docs/gravitino-server-config.md
+++ b/docs/gravitino-server-config.md
@@ -279,20 +279,21 @@ PostgreSQL, and the setup procedure for both is in
 
 The driver, user, and password properties are required whenever the URL is not 
`jdbc:h2`.
 
-| Configuration Item                                 | Description             
                                                                                
                                                                                
                             | Default Value                 |
-|----------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------|
-| `gravitino.entity.store`                           | Entity storage 
implementation. `relational` is the only supported value.                       
                                                                                
                                      | `relational`                  |
-| `gravitino.entity.store.relational`                | Relational storage 
implementation. `JDBCBackend` is the only supported value, and it covers H2, 
MySQL, and PostgreSQL.                                                          
                                     | `JDBCBackend`                 |
-| `gravitino.entity.store.relational.jdbcUrl`        | Database URL the 
backend connects to.                                                            
                                                                                
                                    | `jdbc:h2`                     |
-| `gravitino.entity.store.relational.jdbcDriver`     | Driver class name. 
Place the driver jar in `${GRAVITINO_HOME}/libs/`.                              
                                                                                
                                  | `org.h2.Driver`               |
-| `gravitino.entity.store.relational.jdbcUser`       | Database username.      
                                                                                
                                                                                
                             | `gravitino`                   |
-| `gravitino.entity.store.relational.jdbcPassword`   | Database password.      
                                                                                
                                                                                
                             | `gravitino`                   |
-| `gravitino.entity.store.relational.storagePath`    | Where embedded H2 keeps 
its files. A relative value resolves against `${GRAVITINO_HOME}`. The default 
sits inside the deployment directory, so an upgrade that replaces that 
directory discards the data. Change it. | `${GRAVITINO_HOME}/data/jdbc` |
-| `gravitino.entity.store.relational.maxConnections` | Maximum size of the 
JDBC connection pool.                                                           
                                                                                
                                 | `100`                         |
-| `gravitino.entity.store.relational.maxWaitMillis`  | Maximum wait in 
milliseconds for a connection from the pool.                                    
                                                                                
                                     | `1000`                        |
-| `gravitino.entity.store.maxTransactionSkewTimeMs`  | Maximum transaction 
skew in milliseconds.                                                           
                                                                                
                                 | `2000`                        |
-| `gravitino.entity.store.deleteAfterTimeMs`         | How long in 
milliseconds deleted and superseded rows are kept. Accepts 10 minutes to 30 
days.                                                                           
                                             | `604800000` (7 days)          |
-| `gravitino.entity.store.versionRetentionCount`     | Number of entity 
versions kept, including the current one. Accepts 1 to 10.                      
                                                                                
                                    | `1`                           |
+| Configuration Item                                     | Description         
                                                                                
                                                                                
                                 | Default Value                 |
+|--------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------|
+| `gravitino.entity.store`                               | Entity storage 
implementation. `relational` is the only supported value.                       
                                                                                
                                      | `relational`                  |
+| `gravitino.entity.store.relational`                    | Relational storage 
implementation. `JDBCBackend` is the only supported value, and it covers H2, 
MySQL, and PostgreSQL.                                                          
                                     | `JDBCBackend`                 |
+| `gravitino.entity.store.relational.jdbcUrl`            | Database URL the 
backend connects to.                                                            
                                                                                
                                    | `jdbc:h2`                     |
+| `gravitino.entity.store.relational.jdbcDriver`         | Driver class name. 
Place the driver jar in `${GRAVITINO_HOME}/libs/`.                              
                                                                                
                                  | `org.h2.Driver`               |
+| `gravitino.entity.store.relational.jdbcUser`           | Database username.  
                                                                                
                                                                                
                                 | `gravitino`                   |
+| `gravitino.entity.store.relational.jdbcPassword`       | Database password.  
                                                                                
                                                                                
                                 | `gravitino`                   |
+| `gravitino.entity.store.relational.storagePath`        | Where embedded H2 
keeps its files. A relative value resolves against `${GRAVITINO_HOME}`. The 
default sits inside the deployment directory, so an upgrade that replaces that 
directory discards the data. Change it. | `${GRAVITINO_HOME}/data/jdbc` |
+| `gravitino.entity.store.relational.maxConnections`     | Maximum size of the 
JDBC connection pool.                                                           
                                                                                
                                 | `100`                         |
+| `gravitino.entity.store.relational.maxIdleConnections` | Maximum idle 
connections retained per server; capped by `maxConnections`. Budget for it on 
every server, plus any JDBC catalog pools that use the same database instance.  
                                          | `10`                          |
+| `gravitino.entity.store.relational.maxWaitMillis`      | Maximum wait in 
milliseconds for a connection from the pool.                                    
                                                                                
                                     | `1000`                        |
+| `gravitino.entity.store.maxTransactionSkewTimeMs`      | Maximum transaction 
skew in milliseconds.                                                           
                                                                                
                                 | `2000`                        |
+| `gravitino.entity.store.deleteAfterTimeMs`             | How long in 
milliseconds deleted and superseded rows are kept. Accepts 10 minutes to 30 
days.                                                                           
                                             | `604800000` (7 days)          |
+| `gravitino.entity.store.versionRetentionCount`         | Number of entity 
versions kept, including the current one. Accepts 1 to 10.                      
                                                                                
                                    | `1`                           |
 
 #### Caching
 
diff --git a/docs/how-to-use-relational-backend-storage.md 
b/docs/how-to-use-relational-backend-storage.md
index 80e055d45b..005649f1a1 100644
--- a/docs/how-to-use-relational-backend-storage.md
+++ b/docs/how-to-use-relational-backend-storage.md
@@ -31,6 +31,27 @@ gravitino.entity.store.relational.jdbcPassword = {password}
 `gravitino.entity.store` and `gravitino.entity.store.relational` already 
default to `relational`
 and `JDBCBackend`. Leave them alone.
 
+For concurrent metadata reads, tune 
`gravitino.entity.store.relational.maxIdleConnections`
+alongside `maxConnections`. The default retains up to 10 idle connections per 
server; the
+effective limit never exceeds `maxConnections`. Count this limit once per 
server when calculating
+the database connection budget, and also count the pools of any JDBC catalogs 
that point at the
+same database instance.
+
+This limit, not idle-time eviction, is what decides whether connections are 
reused. When a request
+returns a connection while the pool already holds `maxIdleConnections` idle 
connections, the pool
+closes it at once, and a later request has to open a new one. When the number 
of connections in
+use keeps swinging by more than this limit, physical connections keep being 
closed and reopened,
+which adds latency. Setting it at or above the number of connections in use at 
steady peak
+concurrency avoids that. For example, with 64 concurrent clients listing 
metalakes against MySQL,
+a limit of `10` reopened about 1,400 to 2,700 connections every 15 seconds, 
`32` about 270 to 440,
+and `64` almost none. Raise it only as far as the database connection budget 
of all servers allows.
+
+Idle connections are released slowly. The pool's evictor runs every ten 
minutes and checks at most
+three idle connections per run, closing those idle for more than 30 seconds. 
After a burst, a server
+can therefore keep close to `maxIdleConnections` open for a long time; with a 
limit of `64`, it
+takes more than three hours to shrink back to the minimum of five. Budget for 
the full value on
+every server rather than treating it as a short-lived peak.
+
 The values to use, and the driver each one needs:
 
 | Database            | JDBC URL                                               
           | Driver Class               | Driver Jar                    |
diff --git a/docs/jdbc-clickhouse-catalog.md b/docs/jdbc-clickhouse-catalog.md
index fc86b677aa..c1e17bb59d 100644
--- a/docs/jdbc-clickhouse-catalog.md
+++ b/docs/jdbc-clickhouse-catalog.md
@@ -50,15 +50,16 @@ Pass any JDBC pool property that Gravitino does not define 
by adding the `gravit
 
 When using the JDBC catalog you must provide `jdbc-url`, `jdbc-driver`, 
`jdbc-user`, and `jdbc-password`. Common catalog properties are listed 
[here](./gravitino-server-config.md#catalog-properties-configuration); 
ClickHouse adds no extra catalog-scoped keys.
 
-| Configuration item      | Description                                        
                   | Default value | Required |
-|-------------------------|-----------------------------------------------------------------------|---------------|----------|
-| `jdbc-url`              | JDBC URL, for example 
`jdbc:clickhouse://localhost:8123`              | (none)        | Yes      |
-| `jdbc-driver`           | JDBC driver class, for example 
`com.clickhouse.jdbc.ClickHouseDriver` | (none)        | Yes      |
-| `jdbc-user`             | JDBC user name                                     
                   | (none)        | Yes      |
-| `jdbc-password`         | JDBC password                                      
                   | (none)        | Yes      |
-| `jdbc.pool.min-size`    | Minimum pool size                                  
                   | `2`           | No       |
-| `jdbc.pool.max-size`    | Maximum pool size                                  
                   | `10`          | No       |
-| `jdbc.pool.max-wait-ms` | Max wait time for a connection                     
                   | `30000`       | No       |
+| Configuration item      | Description                                        
                                                                                
                                                      | Default value | 
Required |
+|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|----------|
+| `jdbc-url`              | JDBC URL, for example 
`jdbc:clickhouse://localhost:8123`                                              
                                                                                
   | (none)        | Yes      |
+| `jdbc-driver`           | JDBC driver class, for example 
`com.clickhouse.jdbc.ClickHouseDriver`                                          
                                                                          | 
(none)        | Yes      |
+| `jdbc-user`             | JDBC user name                                     
                                                                                
                                                      | (none)        | Yes     
 |
+| `jdbc-password`         | JDBC password                                      
                                                                                
                                                      | (none)        | Yes     
 |
+| `jdbc.pool.min-size`    | Minimum pool size                                  
                                                                                
                                                      | `2`           | No      
 |
+| `jdbc.pool.max-size`    | Maximum pool size                                  
                                                                                
                                                      | `10`          | No      
 |
+| `jdbc.pool.max-idle`    | Maximum idle connections retained per catalog per 
server; capped by `jdbc.pool.max-size`; takes precedence over 
`gravitino.bypass.maxIdle`. Idle connections are not evicted by default. | `8`  
         | No       |
+| `jdbc.pool.max-wait-ms` | Max wait time for a connection                     
                                                                                
                                                      | `30000`       | No      
 |
 
 ### Create a ClickHouse Catalog
 
diff --git a/docs/jdbc-doris-catalog.md b/docs/jdbc-doris-catalog.md
index 2417f79642..74ea49106b 100644
--- a/docs/jdbc-doris-catalog.md
+++ b/docs/jdbc-doris-catalog.md
@@ -41,15 +41,16 @@ more details.
 
 Besides the [common catalog 
properties](./gravitino-server-config.md#catalog-properties-configuration), the 
Doris catalog has the following properties:
 
-| Configuration item      | Description                                        
                                               | Default value | Required |
-|-------------------------|---------------------------------------------------------------------------------------------------|---------------|----------|
-| `jdbc-url`              | JDBC URL for connecting to the database. For 
example, `jdbc:mysql://localhost:9030`               | (none)        | Yes      
|
-| `jdbc-driver`           | The driver of the JDBC connection. For example, 
`com.mysql.jdbc.Driver`.                          | (none)        | Yes      |
-| `jdbc-user`             | The JDBC user name.                                
                                               | (none)        | Yes      |
-| `jdbc-password`         | The JDBC password.                                 
                                               | (none)        | Yes      |
-| `jdbc.pool.min-size`    | The minimum number of connections in the pool. `2` 
by default.                                    | `2`           | No       |
-| `jdbc.pool.max-size`    | The maximum number of connections in the pool. 
`10` by default.                                   | `10`          | No       |
-| `jdbc.pool.max-wait-ms` | The maximum Duration that the pool will wait for a 
connection to be returned. `30000` by default. | `30000`       | No       |
+| Configuration item      | Description                                        
                                                                                
                                                      | Default value | 
Required |
+|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|----------|
+| `jdbc-url`              | JDBC URL for connecting to the database. For 
example, `jdbc:mysql://localhost:9030`                                          
                                                            | (none)        | 
Yes      |
+| `jdbc-driver`           | The driver of the JDBC connection. For example, 
`com.mysql.jdbc.Driver`.                                                        
                                                         | (none)        | Yes  
    |
+| `jdbc-user`             | The JDBC user name.                                
                                                                                
                                                      | (none)        | Yes     
 |
+| `jdbc-password`         | The JDBC password.                                 
                                                                                
                                                      | (none)        | Yes     
 |
+| `jdbc.pool.min-size`    | The minimum number of connections in the pool. `2` 
by default.                                                                     
                                                      | `2`           | No      
 |
+| `jdbc.pool.max-size`    | The maximum number of connections in the pool. 
`10` by default.                                                                
                                                          | `10`          | No  
     |
+| `jdbc.pool.max-idle`    | Maximum idle connections retained per catalog per 
server; capped by `jdbc.pool.max-size`; takes precedence over 
`gravitino.bypass.maxIdle`. Idle connections are not evicted by default. | `8`  
         | No       |
+| `jdbc.pool.max-wait-ms` | The maximum Duration that the pool will wait for a 
connection to be returned. `30000` by default.                                  
                                                      | `30000`       | No      
 |
 
 Before using the Doris Catalog, you must download the corresponding JDBC 
driver to the `catalogs/jdbc-doris/libs` directory.
 Gravitino doesn't package the JDBC driver for Doris due to licensing issues.
diff --git a/docs/jdbc-hologres-catalog.md b/docs/jdbc-hologres-catalog.md
index 7b6ab04652..5bea59a76d 100644
--- a/docs/jdbc-hologres-catalog.md
+++ b/docs/jdbc-hologres-catalog.md
@@ -43,15 +43,16 @@ Check the relevant data source configuration in [data 
source properties](https:/
 If you use a JDBC catalog, you must provide `jdbc-url`, `jdbc-driver`, 
`jdbc-database`, `jdbc-user` and `jdbc-password` to catalog properties.
 Besides the [common catalog 
properties](./gravitino-server-config.md#catalog-properties-configuration), the 
Hologres catalog has the following properties:
 
-| Configuration item   | Description                                           
                                                                        | 
Default value | Required |
-|----------------------|-------------------------------------------------------------------------------------------------------------------------------|---------------|----------|
-| `jdbc-url`           | JDBC URL for connecting to the database. For example, 
`jdbc:postgresql://hgprecn-cn-xxx.hologres.aliyuncs.com:80/my_database` | 
(none)        | Yes      |
-| `jdbc-driver`        | The driver of the JDBC connection. Must be 
`org.postgresql.Driver`.                                                        
   | (none)        | Yes      |
-| `jdbc-database`      | The database name. This is mandatory for Hologres.    
                                                                        | 
(none)        | Yes      |
-| `jdbc-user`          | The JDBC user name (AccessKey ID or database 
username).                                                                      
 | (none)        | Yes      |
-| `jdbc-password`      | The JDBC password (AccessKey Secret or database 
password).                                                                    | 
(none)        | Yes      |
-| `jdbc.pool.min-size` | The minimum number of connections in the pool. `2` by 
default.                                                                | `2`   
        | No       |
-| `jdbc.pool.max-size` | The maximum number of connections in the pool. `10` 
by default.                                                               | 
`10`          | No       |
+| Configuration item   | Description                                           
                                                                                
                                                   | Default value | Required |
+|----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|----------|
+| `jdbc-url`           | JDBC URL for connecting to the database. For example, 
`jdbc:postgresql://hgprecn-cn-xxx.hologres.aliyuncs.com:80/my_database`         
                                                   | (none)        | Yes      |
+| `jdbc-driver`        | The driver of the JDBC connection. Must be 
`org.postgresql.Driver`.                                                        
                                                              | (none)        | 
Yes      |
+| `jdbc-database`      | The database name. This is mandatory for Hologres.    
                                                                                
                                                   | (none)        | Yes      |
+| `jdbc-user`          | The JDBC user name (AccessKey ID or database 
username).                                                                      
                                                            | (none)        | 
Yes      |
+| `jdbc-password`      | The JDBC password (AccessKey Secret or database 
password).                                                                      
                                                         | (none)        | Yes  
    |
+| `jdbc.pool.min-size` | The minimum number of connections in the pool. `2` by 
default.                                                                        
                                                   | `2`           | No       |
+| `jdbc.pool.max-size` | The maximum number of connections in the pool. `10` 
by default.                                                                     
                                                     | `10`          | No       
|
+| `jdbc.pool.max-idle` | Maximum idle connections retained per catalog per 
server; capped by `jdbc.pool.max-size`; takes precedence over 
`gravitino.bypass.maxIdle`. Idle connections are not evicted by default. | `8`  
         | No       |
 
 :::caution
 Hologres uses the PostgreSQL JDBC Driver (version 42.3.2 or later 
recommended). You need to download the PostgreSQL JDBC Driver and place it in 
the `catalogs/jdbc-hologres/libs` directory under the Gravitino distribution 
(e.g., `distribution/package/catalogs/jdbc-hologres/libs` or 
`distribution/package-all/catalogs/jdbc-hologres/libs`).
diff --git a/docs/jdbc-mysql-catalog.md b/docs/jdbc-mysql-catalog.md
index 12156b4854..9c5126e4ef 100644
--- a/docs/jdbc-mysql-catalog.md
+++ b/docs/jdbc-mysql-catalog.md
@@ -41,15 +41,16 @@ When using Gravitino with Trino, pass the Trino MySQL 
connector configuration us
 If you use a JDBC catalog, you must provide `jdbc-url`, `jdbc-driver`, 
`jdbc-user` and `jdbc-password` to catalog properties.
 Besides the [common catalog 
properties](./gravitino-server-config.md#catalog-properties-configuration), the 
MySQL catalog has the following properties:
 
-| Configuration item      | Description                                        
                                                    | Default value | Required |
-|-------------------------|--------------------------------------------------------------------------------------------------------|---------------|----------|
-| `jdbc-url`              | JDBC URL for connecting to the database. For 
example, `jdbc:mysql://localhost:3306`                    | (none)        | Yes 
     |
-| `jdbc-driver`           | The driver of the JDBC connection. For example, 
`com.mysql.jdbc.Driver` or `com.mysql.cj.jdbc.Driver`. | (none)        | Yes    
  |
-| `jdbc-user`             | The JDBC user name.                                
                                                    | (none)        | Yes      |
-| `jdbc-password`         | The JDBC password.                                 
                                                    | (none)        | Yes      |
-| `jdbc.pool.min-size`    | The minimum number of connections in the pool. `2` 
by default.                                         | `2`           | No       |
-| `jdbc.pool.max-size`    | The maximum number of connections in the pool. 
`10` by default.                                        | `10`          | No    
   |
-| `jdbc.pool.max-wait-ms` | The maximum Duration that the pool will wait for a 
connection to be returned. `30000` by default.      | `30000`       | No       |
+| Configuration item      | Description                                        
                                                                                
                                                      | Default value | 
Required |
+|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|----------|
+| `jdbc-url`              | JDBC URL for connecting to the database. For 
example, `jdbc:mysql://localhost:3306`                                          
                                                            | (none)        | 
Yes      |
+| `jdbc-driver`           | The driver of the JDBC connection. For example, 
`com.mysql.jdbc.Driver` or `com.mysql.cj.jdbc.Driver`.                          
                                                         | (none)        | Yes  
    |
+| `jdbc-user`             | The JDBC user name.                                
                                                                                
                                                      | (none)        | Yes     
 |
+| `jdbc-password`         | The JDBC password.                                 
                                                                                
                                                      | (none)        | Yes     
 |
+| `jdbc.pool.min-size`    | The minimum number of connections in the pool. `2` 
by default.                                                                     
                                                      | `2`           | No      
 |
+| `jdbc.pool.max-size`    | The maximum number of connections in the pool. 
`10` by default.                                                                
                                                          | `10`          | No  
     |
+| `jdbc.pool.max-idle`    | Maximum idle connections retained per catalog per 
server; capped by `jdbc.pool.max-size`; takes precedence over 
`gravitino.bypass.maxIdle`. Idle connections are not evicted by default. | `8`  
         | No       |
+| `jdbc.pool.max-wait-ms` | The maximum Duration that the pool will wait for a 
connection to be returned. `30000` by default.                                  
                                                      | `30000`       | No      
 |
 
 :::caution
 Gravitino does not package the MySQL JDBC driver, because MySQL Connector/J is
diff --git a/docs/jdbc-oceanbase-catalog.md b/docs/jdbc-oceanbase-catalog.md
index 0896fa6983..1d26c0b083 100644
--- a/docs/jdbc-oceanbase-catalog.md
+++ b/docs/jdbc-oceanbase-catalog.md
@@ -39,15 +39,16 @@ Check the relevant data source configuration in [data 
source properties](https:/
 If you use a JDBC catalog, you must provide `jdbc-url`, `jdbc-driver`, 
`jdbc-user` and `jdbc-password` to catalog properties.
 Besides the [common catalog 
properties](./gravitino-server-config.md#catalog-properties-configuration), the 
OceanBase catalog has the following properties:
 
-| Configuration item      | Description                                        
                                                                                
   | Default value | Required |
-|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------|---------------|----------|
-| `jdbc-url`              | JDBC URL for connecting to the database. For 
example, `jdbc:mysql://localhost:2881` or `jdbc:oceanbase://localhost:2881`     
         | (none)        | Yes      |
-| `jdbc-driver`           | The driver of the JDBC connection. For example, 
`com.mysql.jdbc.Driver` or `com.mysql.cj.jdbc.Driver` or 
`com.oceanbase.jdbc.Driver`. | (none)        | Yes      |
-| `jdbc-user`             | The JDBC user name.                                
                                                                                
   | (none)        | Yes      |
-| `jdbc-password`         | The JDBC password.                                 
                                                                                
   | (none)        | Yes      |
-| `jdbc.pool.min-size`    | The minimum number of connections in the pool. `2` 
by default.                                                                     
   | `2`           | No       |
-| `jdbc.pool.max-size`    | The maximum number of connections in the pool. 
`10` by default.                                                                
       | `10`          | No       |
-| `jdbc.pool.max-wait-ms` | The maximum Duration that the pool will wait for a 
connection to be returned. `30000` by default.                                  
   | `30000`       | No       |
+| Configuration item      | Description                                        
                                                                                
                                                      | Default value | 
Required |
+|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|----------|
+| `jdbc-url`              | JDBC URL for connecting to the database. For 
example, `jdbc:mysql://localhost:2881` or `jdbc:oceanbase://localhost:2881`     
                                                            | (none)        | 
Yes      |
+| `jdbc-driver`           | The driver of the JDBC connection. For example, 
`com.mysql.jdbc.Driver` or `com.mysql.cj.jdbc.Driver` or 
`com.oceanbase.jdbc.Driver`.                                                    
| (none)        | Yes      |
+| `jdbc-user`             | The JDBC user name.                                
                                                                                
                                                      | (none)        | Yes     
 |
+| `jdbc-password`         | The JDBC password.                                 
                                                                                
                                                      | (none)        | Yes     
 |
+| `jdbc.pool.min-size`    | The minimum number of connections in the pool. `2` 
by default.                                                                     
                                                      | `2`           | No      
 |
+| `jdbc.pool.max-size`    | The maximum number of connections in the pool. 
`10` by default.                                                                
                                                          | `10`          | No  
     |
+| `jdbc.pool.max-idle`    | Maximum idle connections retained per catalog per 
server; capped by `jdbc.pool.max-size`; takes precedence over 
`gravitino.bypass.maxIdle`. Idle connections are not evicted by default. | `8`  
         | No       |
+| `jdbc.pool.max-wait-ms` | The maximum Duration that the pool will wait for a 
connection to be returned. `30000` by default.                                  
                                                      | `30000`       | No      
 |
 
 :::caution
 Gravitino does not package the OceanBase JDBC driver due to licensing, so you
diff --git a/docs/jdbc-postgresql-catalog.md b/docs/jdbc-postgresql-catalog.md
index 216f6ae8bf..60ff95dd1b 100644
--- a/docs/jdbc-postgresql-catalog.md
+++ b/docs/jdbc-postgresql-catalog.md
@@ -39,16 +39,17 @@ When using Gravitino with Trino, pass the Trino PostgreSQL 
connector configurati
 If you use JDBC catalog, you must provide `jdbc-url`, `jdbc-driver`, 
`jdbc-user` and `jdbc-password` to catalog properties.
 Besides the [common catalog 
properties](./gravitino-server-config.md#catalog-properties-configuration), the 
PostgreSQL catalog has the following properties:
 
-| Configuration item      | Description                                        
                                                                                
                               | Default value | Required |
-|-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|----------|
-| `jdbc-url`              | A valid PostgreSQL JDBC URL, for example 
`jdbc:postgresql://localhost:5432/pg_database?sslmode=require`. If the URL 
omits the database, set `jdbc-database`. | (none)        | Yes      |
-| `jdbc-driver`           | The driver of the JDBC connection. For example 
`org.postgresql.Driver`.                                                        
                                   | (none)        | Yes      |
-| `jdbc-database`         | The database of the JDBC connection. Derived from 
`jdbc-url` when omitted. An explicit value must be nonblank and match the URL 
database when both are provided. | (none)        | Only if absent from 
`jdbc-url` |
-| `jdbc-user`             | The JDBC user name.                                
                                                                                
                               | (none)        | Yes      |
-| `jdbc-password`         | The JDBC password.                                 
                                                                                
                               | (none)        | Yes      |
-| `jdbc.pool.min-size`    | The minimum number of connections in the pool. `2` 
by default.                                                                     
                               | `2`           | No       |
-| `jdbc.pool.max-size`    | The maximum number of connections in the pool. 
`10` by default.                                                                
                                   | `10`          | No       |
-| `jdbc.pool.max-wait-ms` | The maximum Duration that the pool will wait for a 
connection to be returned. `30000` by default.                                  
                               | `30000`       | No       |
+| Configuration item      | Description                                        
                                                                                
                                                      | Default value | 
Required                       |
+|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|--------------------------------|
+| `jdbc-url`              | A valid PostgreSQL JDBC URL, for example 
`jdbc:postgresql://localhost:5432/pg_database?sslmode=require`. If the URL 
omits the database, set `jdbc-database`.                             | (none)   
     | Yes                            |
+| `jdbc-driver`           | The driver of the JDBC connection. For example 
`org.postgresql.Driver`.                                                        
                                                          | (none)        | Yes 
                           |
+| `jdbc-database`         | The database of the JDBC connection. Derived from 
`jdbc-url` when omitted. An explicit value must be nonblank and match the URL 
database when both are provided.                         | (none)        | Only 
if absent from `jdbc-url` |
+| `jdbc-user`             | The JDBC user name.                                
                                                                                
                                                      | (none)        | Yes     
                       |
+| `jdbc-password`         | The JDBC password.                                 
                                                                                
                                                      | (none)        | Yes     
                       |
+| `jdbc.pool.min-size`    | The minimum number of connections in the pool. `2` 
by default.                                                                     
                                                      | `2`           | No      
                       |
+| `jdbc.pool.max-size`    | The maximum number of connections in the pool. 
`10` by default.                                                                
                                                          | `10`          | No  
                           |
+| `jdbc.pool.max-idle`    | Maximum idle connections retained per catalog per 
server; capped by `jdbc.pool.max-size`; takes precedence over 
`gravitino.bypass.maxIdle`. Idle connections are not evicted by default. | `8`  
         | No                             |
+| `jdbc.pool.max-wait-ms` | The maximum Duration that the pool will wait for a 
connection to be returned. `30000` by default.                                  
                                                      | `30000`       | No      
                       |
 
 :::caution
 Download the corresponding JDBC driver to the `catalogs/jdbc-postgresql/libs` 
directory.
diff --git a/docs/jdbc-starrocks-catalog.md b/docs/jdbc-starrocks-catalog.md
index d3c976c450..f23221a632 100644
--- a/docs/jdbc-starrocks-catalog.md
+++ b/docs/jdbc-starrocks-catalog.md
@@ -40,15 +40,16 @@ more details.
 
 Besides the [common catalog 
properties](./gravitino-server-config.md#catalog-properties-configuration), the 
StarRocks catalog has the following properties:
 
-| Configuration item      | Description                                        
                                               | Default value | Required |
-|-------------------------|---------------------------------------------------------------------------------------------------|---------------|----------|
-| `jdbc-url`              | JDBC URL for connecting to the database. For 
example, `jdbc:mysql://localhost:9030`               | (none)        | Yes      
|
-| `jdbc-driver`           | The driver of the JDBC connection. For example, 
`com.mysql.jdbc.Driver`.                          | (none)        | Yes      |
-| `jdbc-user`             | The JDBC user name.                                
                                               | (none)        | Yes      |
-| `jdbc-password`         | The JDBC password.                                 
                                               | (none)        | Yes      |
-| `jdbc.pool.min-size`    | The minimum number of connections in the pool. `2` 
by default.                                    | `2`           | No       |
-| `jdbc.pool.max-size`    | The maximum number of connections in the pool. 
`10` by default.                                   | `10`          | No       |
-| `jdbc.pool.max-wait-ms` | The maximum Duration that the pool will wait for a 
connection to be returned. `30000` by default. | `30000`       | No       |
+| Configuration item      | Description                                        
                                                                                
                                                      | Default value | 
Required |
+|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|----------|
+| `jdbc-url`              | JDBC URL for connecting to the database. For 
example, `jdbc:mysql://localhost:9030`                                          
                                                            | (none)        | 
Yes      |
+| `jdbc-driver`           | The driver of the JDBC connection. For example, 
`com.mysql.jdbc.Driver`.                                                        
                                                         | (none)        | Yes  
    |
+| `jdbc-user`             | The JDBC user name.                                
                                                                                
                                                      | (none)        | Yes     
 |
+| `jdbc-password`         | The JDBC password.                                 
                                                                                
                                                      | (none)        | Yes     
 |
+| `jdbc.pool.min-size`    | The minimum number of connections in the pool. `2` 
by default.                                                                     
                                                      | `2`           | No      
 |
+| `jdbc.pool.max-size`    | The maximum number of connections in the pool. 
`10` by default.                                                                
                                                          | `10`          | No  
     |
+| `jdbc.pool.max-idle`    | Maximum idle connections retained per catalog per 
server; capped by `jdbc.pool.max-size`; takes precedence over 
`gravitino.bypass.maxIdle`. Idle connections are not evicted by default. | `8`  
         | No       |
+| `jdbc.pool.max-wait-ms` | The maximum Duration that the pool will wait for a 
connection to be returned. `30000` by default.                                  
                                                      | `30000`       | No      
 |
 
 Before using the StarRocks Catalog, you must download the corresponding JDBC 
driver to the `catalogs/jdbc-starrocks/libs` directory.
 Gravitino doesn't package the JDBC driver for StarRocks due to licensing 
issues.

Reply via email to