Repository: apache/jena (2025-07-20T17:35:07Z)

Issue:
COALESCE function has poor performance compared to IF(BOUND(...), ..., ...) pattern
### Version

5.6.0-SNAPSHOT

### Feature

We've observed that the SPARQL COALESCE function in Jena performs significantly slower than an equivalent IF(BOUND(...), ..., ...) pattern. This appears to be due to COALESCE's implementation using exception handling for unbound variables.

**Steps to Reproduce:**

Consider these two functionally equivalent queries:

Query 1 (using COALESCE - slower):
```sparql
SELECT ?person (COALESCE(?nickname, ?fullname) as ?displayName)
WHERE {
    ?person :hasFullName ?fullname .
    OPTIONAL { ?person :hasNickname ?nickname . }
}
```

Query 2 (using IF/BOUND - faster):
```sparql
SELECT ?person ?displayName
WHERE {
    ?person :hasFullName ?fullname .
    OPTIONAL { ?person :hasNickname ?nickname . }
    BIND(IF(BOUND(?nickname), ?nickname, ?fullname) as ?displayName)
}
```

Both queries return the nickname if available, otherwise the full name. However, Query 2 performs significantly better than Query 1.

**Expected Behavior:**

COALESCE should have comparable performance to the IF(BOUND(...), ..., ...) pattern for simple cases involving unbound variables.

**Actual Behavior:**

COALESCE is noticeably slower, particularly when the first arguments are frequently unbound.

**Analysis:**

Based on my understanding, COALESCE evaluates each expression and catches any `ExprEvalException` before moving to the next expression. This exception handling overhead appears to be the cause of the performance degradation, especially when dealing with OPTIONAL patterns where variables are often unbound.

**Suggested Solution:**

Extend `org.apache.jena.sparql.expr.Expr` with a method to allow fast pre-evaluation checks:

```java
public boolean isLikelyToEvaluate(Binding binding, FunctionEnv env) {
    return true; // default implementation for backward compatibility
}
```

Then implementations like `ExprVar` could override it to perform a fast bound check:
```java
@Override
public boolean isLikelyToEvaluate(Binding binding, FunctionEnv env) {
    return binding != null && binding.contains(var);
}
```

This would allow COALESCE to pre-check expressions before attempting evaluation, avoiding the exception handling overhead for unbound variables. The COALESCE implementation could then use this method to skip expressions that would throw exceptions, only evaluating those that can succeed.

### Are you interested in contributing a solution yourself?

Perhaps?


Discussion:
Great analysis!



However, I'm not so keen on `isLikelyToEvaluate` as an approach because it's for all cases and all `Expr` - a wider contract.
Instead, change `E_Coalesce` to optimize a known case.

```java
    @Override
    public NodeValue evalSpecial(Binding binding, FunctionEnv env) {
        for ( Expr expr : super.getArgs() ) {
            if ( binding != null && expr.isVariable() ) {
                // The case of an argument being a variable.
                // Do directly - COALESCE(?X, ...) is a common usage
                // pattern and this avoids relying on an exception. 
                if ( binding == null )
                    continue;
                Var var = expr.getExprVar().asVar();
                if ( ! binding.contains(var) )
                    continue;
                return expr.eval(binding, env);
            }
            try {
                NodeValue nv = expr.eval(binding, env);
                return nv;
            } catch (ExprEvalException ex) {}
        }
        throw new ExprEvalException("COALESCE: no value");
    }
```

By the way - I'd like to understand the costs. `ExprEvalException` is (should) already avoid creating the stacktrace for the exception. The actual exception is `VariableNotBoundException`, a subclass of  `ExprEvalException`.

Is it the `try{}` block itself, or the catching the exception that is the cost point? and is using a subclass a factor?
Looking at the profiler recordings, the main work goes into construction of the exceptions:

<img width="1113" height="716" alt="Image" src="https://github.com/user-attachments/assets/0f2dbeb7-cc91-4431-ba83-2b20461bc335" />

In this part of Jena I had no clue if there were other expressions or so, where the fail fast approach could be beneficial compared to constructing and catching exceptions.



Patch:
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/expr/E_Coalesce.java b/jena-arq/src/main/java/org/apache/jena/sparql/expr/E_Coalesce.java
index 23d47304d24..588c9f5e9ab 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/expr/E_Coalesce.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/expr/E_Coalesce.java
@@ -37,9 +37,15 @@ public E_Coalesce(ExprList args) {
     @Override
     public NodeValue evalSpecial(Binding binding, FunctionEnv env) {
         for ( Expr expr : super.getArgs() ) {
+            if(expr.isVariable() && (binding == null || !binding.contains(expr.asVar()))) {
+                // If the expression is a variable and no binding is provided,
+                // we skip it as it cannot be evaluated.
+                // COALESCE(?X, ...) is a common usage pattern and this check
+                // avoids creating an exception for each unbound variable.
+                continue;
+            }
             try {
-                NodeValue nv = expr.eval(binding, env);
-                return nv;
+                return expr.eval(binding, env);
             } catch (ExprEvalException ex) {}
         }
         throw new ExprEvalException("COALESCE: no value");
diff --git a/jena-benchmarks/jena-benchmarks-jmh/pom.xml b/jena-benchmarks/jena-benchmarks-jmh/pom.xml
index ff1cc0ea7db..f41c4185291 100644
--- a/jena-benchmarks/jena-benchmarks-jmh/pom.xml
+++ b/jena-benchmarks/jena-benchmarks-jmh/pom.xml
@@ -100,6 +100,13 @@
             <scope>test</scope>
         </dependency>
 
+        <dependency>
+            <groupId>org.apache.jena</groupId>
+            <artifactId>jena-benchmarks-shadedJena550</artifactId>
+            <version>5.6.0-SNAPSHOT</version>
+            <scope>test</scope>
+        </dependency>
+
         <dependency>
             <groupId>org.apache.logging.log4j</groupId>
             <artifactId>log4j-slf4j2-impl</artifactId>
diff --git a/jena-benchmarks/jena-benchmarks-shadedJena510/pom.xml b/jena-benchmarks/jena-benchmarks-shadedJena510/pom.xml
index 8710e03627e..c1ddc5bea22 100644
--- a/jena-benchmarks/jena-benchmarks-shadedJena510/pom.xml
+++ b/jena-benchmarks/jena-benchmarks-shadedJena510/pom.xml
@@ -45,7 +45,7 @@
 
     <properties>
         <build.time.xsd>${maven.build.timestamp}</build.time.xsd>
-        <automatic.module.name>org.apache.jena.benchmarks.shadedJena480</automatic.module.name>
+        <automatic.module.name>org.apache.jena.benchmarks.shadedJena510</automatic.module.name>
         <maven.test.skip>true</maven.test.skip>
         <scope>test</scope>
     </properties>
diff --git a/jena-benchmarks/jena-benchmarks-shadedJena550/pom.xml b/jena-benchmarks/jena-benchmarks-shadedJena550/pom.xml
new file mode 100644
index 00000000000..bad88087286
--- /dev/null
+++ b/jena-benchmarks/jena-benchmarks-shadedJena550/pom.xml
@@ -0,0 +1,138 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.jena</groupId>
+        <artifactId>jena-benchmarks</artifactId>
+        <version>5.6.0-SNAPSHOT</version>
+    </parent>
+
+    <name>Apache Jena - Benchmarks Shaded Jena 5.5.0</name>
+    <artifactId>jena-benchmarks-shadedJena550</artifactId>
+    <description>Shaded Apache Jena 5.5.0 for regression benchmarks</description>
+    <packaging>jar</packaging>
+
+    <url>https://jena.apache.org/</url>
+
+    <organization>
+        <name>Apache Jena</name>
+        <url>https://jena.apache.org/</url>
+    </organization>
+
+    <licenses>
+        <license>
+            <name>Apache 2.0 License</name>
+            <url>https://www.apache.org/licenses/LICENSE-2.0</url>
+        </license>
+    </licenses>
+
+    <properties>
+        <build.time.xsd>${maven.build.timestamp}</build.time.xsd>
+        <automatic.module.name>org.apache.jena.benchmarks.shadedJena550</automatic.module.name>
+        <maven.test.skip>true</maven.test.skip>
+        <scope>test</scope>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.jena</groupId>
+            <artifactId>jena-base</artifactId>
+            <version>5.5.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.jena</groupId>
+            <artifactId>jena-iri</artifactId>
+            <version>5.5.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.jena</groupId>
+            <artifactId>jena-iri3986</artifactId>
+            <version>5.5.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.jena</groupId>
+            <artifactId>jena-core</artifactId>
+            <version>5.5.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.jena</groupId>
+            <artifactId>jena-arq</artifactId>
+            <version>5.5.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.jena</groupId>
+            <artifactId>jena-geosparql</artifactId>
+            <version>5.5.0</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-shade-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>shade</goal>
+                        </goals>
+                        <configuration>
+                            <createDependencyReducedPom>false</createDependencyReducedPom>
+                            <transformers>
+                              <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
+                              <transformer implementation="org.apache.maven.plugins.shade.resource.ApacheLicenseResourceTransformer" />
+                              <transformer implementation="org.apache.maven.plugins.shade.resource.ApacheNoticeResourceTransformer">
+                                <addHeader>false</addHeader>
+                              </transformer>
+                            </transformers>
+                            <artifactSet>
+                                <includes>
+                                    <include>*:*</include>
+                                </includes>
+                            </artifactSet>
+                            <relocations>
+                                <relocation>
+                                    <pattern>org.apache.jena</pattern>
+                                    <shadedPattern>org.apache.shadedJena550</shadedPattern>
+                                </relocation>
+                            </relocations>
+                            <filters>
+                                <filter>
+                                    <artifact>*:*</artifact>
+                                    <excludes>
+                                        <exclude>META-INF/*.SF</exclude>
+                                        <exclude>META-INF/*.DSA</exclude>
+                                        <exclude>META-INF/*.RSA</exclude>
+                                        <exclude>META-INF/DEPENDENCIES</exclude>
+                                        <exclude>META-INF/MANIFEST.MF</exclude>
+                                        <exclude>**/module-info.class</exclude>
+                                        <exclude>META-INF/LICENSE-notice.md</exclude>
+                                    </excludes>
+                                </filter>
+                            </filters>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git a/jena-benchmarks/pom.xml b/jena-benchmarks/pom.xml
index 7a0094ded1d..e44cc884baf 100644
--- a/jena-benchmarks/pom.xml
+++ b/jena-benchmarks/pom.xml
@@ -50,6 +50,7 @@
     <module>jena-benchmarks-jmh</module>
     <module>jena-benchmarks-shadedJena480</module>
     <module>jena-benchmarks-shadedJena510</module>
+    <module>jena-benchmarks-shadedJena550</module>
   </modules>
 
 </project>


Test patch:
diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/sparql/expr/TestCoalesce.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/sparql/expr/TestCoalesce.java
new file mode 100644
index 00000000000..4bf57be1084
--- /dev/null
+++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/sparql/expr/TestCoalesce.java
@@ -0,0 +1,115 @@
+/*
+ * 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.
+ */
+
+package org.apache.jena.sparql.expr;
+
+
+import org.apache.jena.graph.NodeFactory;
+import org.apache.jena.mem.graph.helper.JMHDefaultOptions;
+import org.apache.jena.sparql.engine.binding.Binding;
+import org.apache.jena.sparql.function.FunctionEnv;
+import org.apache.jena.sys.JenaSystem;
+import org.junit.Assert;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.runner.Runner;
+
+import java.util.Random;
+
+
+@State(Scope.Benchmark)
+public class TestCoalesce {
+
+    static {
+        JenaSystem.init();
+        org.apache.shadedJena550.sys.JenaSystem.init();
+    }
+
+    final static Random rnd = new Random();
+
+    @Benchmark
+    public Long benchmarkCoalesceJenaCurrent() {
+        var checksum = 0L;
+        // Create a Coalesce expression with some dummy expressions
+        var expressions = new ExprList();
+        expressions.add(new ExprVar("var1"));
+        expressions.add(new ExprVar("var2"));
+        expressions.add(new ExprVar("var3"));
+        expressions.add(new ExprVar("var4"));
+        expressions.add(new ExprVar("var5"));
+
+        var var3BoundLiteral =  NodeFactory.createLiteralString("Value5");
+        var bindingBuilder = Binding.builder();
+        bindingBuilder.add(expressions.get(4).asVar(), var3BoundLiteral);
+        var binding = bindingBuilder.build();
+
+        FunctionEnv env = null;
+
+        // Coalesce expression
+        var coalesceExpr = new E_Coalesce(expressions);
+
+        for(var i=0; i<1000000; i++) {
+            var result = coalesceExpr.eval(binding, env);
+            if( result != null ) {
+                checksum += i;
+            }
+        }
+        return checksum;
+    }
+
+    @Benchmark
+    public Long benchmarkCoalesceJena550() {
+        var checksum = 0L;
+        // Create a Coalesce expression with some dummy expressions
+        var expressions = new org.apache.shadedJena550.sparql.expr.ExprList();
+        expressions.add(new org.apache.shadedJena550.sparql.expr.ExprVar("var1"));
+        expressions.add(new org.apache.shadedJena550.sparql.expr.ExprVar("var2"));
+        expressions.add(new org.apache.shadedJena550.sparql.expr.ExprVar("var3"));
+        expressions.add(new org.apache.shadedJena550.sparql.expr.ExprVar("var4"));
+        expressions.add(new org.apache.shadedJena550.sparql.expr.ExprVar("var5"));
+
+        var var3BoundLiteral =  org.apache.shadedJena550.graph.NodeFactory.createLiteralString("Value5");
+        var bindingBuilder = org.apache.shadedJena550.sparql.engine.binding.Binding.builder();
+        bindingBuilder.add(expressions.get(4).asVar(), var3BoundLiteral);
+        var binding = bindingBuilder.build();
+        org.apache.shadedJena550.sparql.function.FunctionEnv env = null;
+
+        // Coalesce expression
+        var coalesceExpr =
+                new org.apache.shadedJena550.sparql.expr.E_Coalesce(expressions);
+
+        for(var i=0; i<1000000; i++) {
+            var result = coalesceExpr.eval(binding, env);
+            if( result != null ) {
+                checksum += i;
+            }
+        }
+        return checksum;
+    }
+
+    @Test
+    public void benchmark() throws Exception {
+        var opt = JMHDefaultOptions.getDefaults(this.getClass())
+                .build();
+        var results = new Runner(opt).run();
+        Assert.assertNotNull(results);
+    }
+}