Subject: [pgjdbc/pgjdbc] issue #3865: Avoid well known slow JDK libraries String.format() and DecimalFormat in PGInterval::getValue to get drastic performance boost

From: "lukaseder (@lukaseder)" <[email-masked]>
Date: Thu, 13 Nov 2025 16:40:42 +0000

**Describe the issue**

`PGInterval::getValue` uses `Stirng::format` and `DecimalFormat` to format the interval value to the PostgreSQL notation:

```java
  public @Nullable String getValue() {
    if (isNull) {
      return null;
    }
    DecimalFormat df = (DecimalFormat) NumberFormat.getInstance(Locale.US);
    df.applyPattern("0.0#####");

    return String.format(
      Locale.ROOT,
      "%d years %d mons %d days %d hours %d mins %s secs",
      years,
      months,
      days,
      hours,
      minutes,
      df.format(getSeconds())
    );
  }
```

This performs terribly and should be avoided. I'm suggesting this alternative implementation, which should do the same thing (except it always renders trailing zeros):

```java
  public String getValue() {
    if (isNull) {
      return null;
    }

    StringBuilder sb = new StringBuilder();
    sb.append(years).append(" years ")
      .append(months).append(" mons ")
      .append(days).append(" days ")
      .append(hours).append(" hours ")
      .append(minutes).append(" mins ");

    if (wholeSeconds < 0 || microSeconds < 0)
      sb.append('-');

    sb.append(Math.abs(wholeSeconds));

    if (microSeconds != 0) {
      sb.append('.');
      String s = "" + Math.abs(microSeconds);

      for (int i = s.length(); i < 6; i++)
        sb.append('0');

      sb.append(s);
    }

    sb.append(" secs");
    return sb.toString();
  }
```

**Driver Version?** 

42.7.8

**Java Version?**

openjdk version "21.0.7" 2025-04-15 LTS

**OS Version?**

Microsoft Windows [Version 10.0.26100.7171]

**PostgreSQL Version?**

N/A

**To Reproduce**

Run this JMH benchmark:

```java
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.TearDown;
import org.openjdk.jmh.annotations.Warmup;

@Fork(value = 3)
@Warmup(iterations = 3, time = 3)
@Measurement(iterations = 5, time = 3)
public class PGIntervalBenchmark {

    @State(Scope.Benchmark)
    public static class BenchmarkState {

        PGInterval interval;

        @Setup(Level.Trial)
        public void setup() throws Exception {
            interval = new PGInterval(0, 0, 0, 0, 0, 0);
        }

        @TearDown(Level.Trial)
        public void teardown() throws Exception {
            interval = null;
        }
    }

    @Benchmark
    public String testGetValue(BenchmarkState state) {
        return state.interval.getValue();
    }
}
```

**Expected behaviour**

The current version produces these benchmark results:

```
PGIntervalBenchmark.testGetValue  thrpt   15   1447069.902 ± 67832.795  ops/s
```

My version produces a 32x improvement:

```
PGIntervalBenchmark.testGetValue  thrpt   15  46957485.917 ± 497298.690  ops/s
```

---

From: "davecramer (@davecramer)" <[email-masked]>
Date: Thu, 13 Nov 2025 16:56:01 +0000

Well the except part is kinda important. It will probably break something

---

From: "vlsi (@vlsi)" <[email-masked]>
Date: Thu, 13 Nov 2025 17:04:36 +0000

I agree there's no point in using `String.format` on the hot path.

---

From: "lukaseder (@lukaseder)" <[email-masked]>
Date: Fri, 14 Nov 2025 08:08:55 +0000

> Well the except part is kinda important. It will probably break something

Sure, you can remove the trailing zeros like this:

```java
while (sb.charAt(sb.length() - 1) == '0')
  sb.deleteCharAt(sb.length() - 1);
```

So:

```java

    if (microSeconds != 0) {
      sb.append('.');

      sb.append(s);

      while (sb.charAt(sb.length() - 1) == '0')
        sb.deleteCharAt(sb.length() - 1);
    }

    sb.append(" secs");
    return sb.toString();
  }
```