diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndex.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndex.java index 914881c..cd9c1d4 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndex.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndex.java @@ -30,6 +30,8 @@ import org.apache.jackrabbit.oak.spi.query.Filter; import org.apache.jackrabbit.oak.spi.query.Filter.PropertyRestriction; import org.apache.jackrabbit.oak.spi.query.QueryIndex; import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.google.common.base.Charsets; import com.google.common.collect.Iterables; @@ -90,6 +92,8 @@ class PropertyIndex implements QueryIndex { */ private static final String EMPTY_TOKEN = ":"; + private static final Logger LOG = LoggerFactory.getLogger(PropertyIndex.class); + static Set encode(PropertyValue value) { if (value == null) { return null; @@ -113,6 +117,37 @@ class PropertyIndex implements QueryIndex { return values; } + private Cheapest findCheapestProperty(Filter filter, PropertyIndexLookup lookup) { + Cheapest cost = new Cheapest(); + for (PropertyRestriction pr : filter.getPropertyRestrictions()) { + String propertyName = PathUtils.getName(pr.propertyName); + double propertyCost = Double.POSITIVE_INFINITY; + // TODO support indexes on a path + // currently, only indexes on the root node are supported + if (lookup.isIndexed(propertyName, "/", filter)) { + if (pr.firstIncluding && pr.lastIncluding + && pr.first != null && pr.first.equals(pr.last)) { + // "[property] = $value" + propertyCost = lookup.getCost(filter, propertyName, pr.first); + } else if (pr.list != null) { + propertyCost = 0; + for (PropertyValue p : pr.list) { + propertyCost += lookup.getCost(filter, propertyName, p); + } + } else { + // processed as "[property] is not null" + propertyCost = lookup.getCost(filter, propertyName, null); + } + } + LOG.debug("property cost for {} is {}", propertyName, propertyCost); + if (propertyCost < cost.cost) { + cost.cost = propertyCost; + cost.propertyRestriction = pr; + } + } + return cost; + } + //--------------------------------------------------------< QueryIndex >-- @Override @@ -142,29 +177,9 @@ class PropertyIndex implements QueryIndex { } PropertyIndexLookup lookup = getLookup(root); - for (PropertyRestriction pr : filter.getPropertyRestrictions()) { - String propertyName = PathUtils.getName(pr.propertyName); - // TODO support indexes on a path - // currently, only indexes on the root node are supported - if (lookup.isIndexed(propertyName, "/", filter)) { - if (pr.firstIncluding && pr.lastIncluding - && pr.first != null && pr.first.equals(pr.last)) { - // "[property] = $value" - return lookup.getCost(filter, propertyName, pr.first); - } else if (pr.list != null) { - double cost = 0; - for (PropertyValue p : pr.list) { - cost += lookup.getCost(filter, propertyName, p); - } - return cost; - } else { - // processed as "[property] is not null" - return lookup.getCost(filter, propertyName, null); - } - } - } - // not an appropriate index - return Double.POSITIVE_INFINITY; + Cheapest cheapest = findCheapestProperty(filter, lookup); + LOG.debug("Cheapest property cost is {} for property {}", cheapest.cost, cheapest.propertyRestriction != null ? cheapest.propertyRestriction.propertyName : null); + return cheapest.cost; } @Override @@ -173,7 +188,11 @@ class PropertyIndex implements QueryIndex { PropertyIndexLookup lookup = getLookup(root); int depth = 1; - for (PropertyRestriction pr : filter.getPropertyRestrictions()) { + + Cheapest cheapest = findCheapestProperty(filter, lookup); + PropertyRestriction pr = cheapest.propertyRestriction; + + if (pr != null) { String propertyName = PathUtils.getName(pr.propertyName); depth = PathUtils.getDepth(pr.propertyName); // TODO support indexes on a path @@ -184,7 +203,6 @@ class PropertyIndex implements QueryIndex { && pr.first != null && pr.first.equals(pr.last)) { // "[property] = $value" paths = lookup.query(filter, propertyName, pr.first); - break; } else if (pr.list != null) { for (PropertyValue pv : pr.list) { Iterable p = lookup.query(filter, propertyName, pv); @@ -194,11 +212,9 @@ class PropertyIndex implements QueryIndex { paths = Iterables.concat(paths, p); } } - break; } else { // processed as "[property] is not null" paths = lookup.query(filter, propertyName, null); - break; } } } @@ -217,19 +233,29 @@ class PropertyIndex implements QueryIndex { StringBuilder buff = new StringBuilder("property"); StringBuilder notIndexed = new StringBuilder(); PropertyIndexLookup lookup = getLookup(root); + Cheapest cheapest = findCheapestProperty(filter, lookup); + PropertyRestriction cheapestPr = cheapest.propertyRestriction; + for (PropertyRestriction pr : filter.getPropertyRestrictions()) { String propertyName = PathUtils.getName(pr.propertyName); // TODO support indexes on a path // currently, only indexes on the root node are supported if (lookup.isIndexed(propertyName, "/", filter)) { + if (cheapestPr == pr) { + buff.append(" usedIndex["); + } else { + buff.append(" unusedIndex["); + } + if (pr.firstIncluding && pr.lastIncluding && pr.first != null && pr.first.equals(pr.last)) { - buff.append(' ').append(propertyName).append('=').append(pr.first); + buff.append(propertyName).append('=').append(pr.first); } else { - buff.append(' ').append(propertyName); + buff.append(propertyName); } + buff.append(']'); } else if (pr.list != null) { - buff.append(' ').append(propertyName).append(" IN("); + buff.append(" list[").append(propertyName).append(" IN("); int i = 0; for (PropertyValue pv : pr.list) { if (i++ > 0) { @@ -237,18 +263,24 @@ class PropertyIndex implements QueryIndex { } buff.append(pv); } - buff.append(')'); + buff.append(')').append(']'); } else { notIndexed.append(' ').append(propertyName); - if (!pr.toString().isEmpty()) { - notIndexed.append(':').append(pr); + if (pr.firstIncluding && pr.lastIncluding + && pr.first != null && pr.first.equals(pr.last)) { + notIndexed.append('=').append(pr.first); } } } if (notIndexed.length() > 0) { - buff.append(" (").append(notIndexed.toString().trim()).append(")"); + buff.append(" notIndexed[").append(notIndexed.toString().trim()).append(']'); } return buff.toString(); } + private static class Cheapest { + private double cost = Double.POSITIVE_INFINITY; + private PropertyRestriction propertyRestriction; + } + } \ No newline at end of file diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexQueryTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexQueryTest.java index bf59c6f..01d4105 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexQueryTest.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexQueryTest.java @@ -33,7 +33,6 @@ import org.apache.jackrabbit.oak.api.ContentRepository; import org.apache.jackrabbit.oak.api.PropertyValue; import org.apache.jackrabbit.oak.api.ResultRow; import org.apache.jackrabbit.oak.api.Tree; -import org.apache.jackrabbit.oak.plugins.index.nodetype.NodeTypeIndexProvider; import org.apache.jackrabbit.oak.plugins.nodetype.write.InitialContent; import org.apache.jackrabbit.oak.query.AbstractQueryTest; import org.apache.jackrabbit.oak.spi.lifecycle.RepositoryInitializer; @@ -57,6 +56,18 @@ public class PropertyIndexQueryTest extends AbstractQueryTest { .with(new OpenSecurityProvider()) .with(new PropertyIndexProvider()) .with(new PropertyIndexEditorProvider()) + .with(new RepositoryInitializer() { + + @Override + public void initialize(NodeBuilder builder) { + createIndexDefinition( + builder.child(INDEX_DEFINITIONS_NAME), + "indexedProperty1", + true, + false, + ImmutableSet.of("indexedProperty1"), null); + } + }) .createContentRepository(); } diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/RelativePathTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/RelativePathTest.java index 9807390..f83a887 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/RelativePathTest.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/RelativePathTest.java @@ -74,7 +74,7 @@ public class RelativePathTest extends AbstractQueryTest { Query.JCR_SQL2); assertEquals(1, lines.size()); // make sure it used the property index - assertTrue(lines.get(0).contains("property myProp")); + assertTrue(lines.get(0).contains("property usedIndex[myProp]")); assertQuery( "select [jcr:path] from [nt:base] where [n/myProp] = 'foo'", diff --git a/oak-core/src/test/resources/org/apache/jackrabbit/oak/query/sql2_index.txt b/oak-core/src/test/resources/org/apache/jackrabbit/oak/query/sql2_index.txt index 92d8faf..27e3ec1 100644 --- a/oak-core/src/test/resources/org/apache/jackrabbit/oak/query/sql2_index.txt +++ b/oak-core/src/test/resources/org/apache/jackrabbit/oak/query/sql2_index.txt @@ -27,7 +27,7 @@ explain select * from [nt:base] where [jcr:uuid] like '%' -[nt:base] as [nt:base] /* property jcr:uuid +[nt:base] as [nt:base] /* property usedIndex[jcr:uuid] where [nt:base].[jcr:uuid] like cast('%' as string) */ explain select e.[jcr:path] @@ -42,7 +42,7 @@ explain select e.[jcr:path] and name(c) = 'd' and name(d) = 'e' and (e.[jcr:uuid] = '1' or e.[jcr:uuid] = '2' or e.[jcr:uuid] = '3' or e.[jcr:uuid] = '4') -[nt:base] as [e] /* property jcr:uuid +[nt:base] as [e] /* property usedIndex[jcr:uuid] where ([e].[jcr:uuid] is not null) and ([e].[jcr:uuid] in(cast('1' as string), cast('2' as string), cast('3' as string), cast('4' as string))) */ inner join [nt:base] as [d] /* traverse "* && //parent/of/join" @@ -71,7 +71,7 @@ explain select e.[jcr:path] and name(c) = 'd' and name(d) = 'e' and (e.[jcr:uuid] = '1' or e.[jcr:uuid] = '2' or e.[jcr:uuid] = '3' or e.[jcr:uuid] = '4') -[nt:base] as [e] /* property jcr:uuid +[nt:base] as [e] /* property usedIndex[jcr:uuid] where ([e].[jcr:uuid] is not null) and ([e].[jcr:uuid] in(cast('1' as string), cast('2' as string), cast('3' as string), cast('4' as string))) */ inner join [nt:base] as [d] /* traverse "* && //parent/of/join" @@ -117,7 +117,7 @@ explain select b.[jcr:uuid] from [nt:base] as a inner join [nt:base] as b on isdescendantnode(b, a) where (a.[jcr:uuid] = '1' or a.[jcr:uuid] = '2') -[nt:base] as [a] /* property jcr:uuid +[nt:base] as [a] /* property usedIndex[jcr:uuid] where ([a].[jcr:uuid] is not null) and ([a].[jcr:uuid] in(cast('1' as string), cast('2' as string))) */ inner join [nt:base] as [b] /* traverse "* && //path/from/join//*" */ @@ -128,50 +128,50 @@ explain select b.[jcr:uuid] inner join [nt:base] as b on isdescendantnode(b, a) where (a.[jcr:uuid] = '1' or a.[jcr:uuid] = '2') and b.[jcr:uuid] is not null -[nt:base] as [a] /* property jcr:uuid +[nt:base] as [a] /* property usedIndex[jcr:uuid] where ([a].[jcr:uuid] is not null) and ([a].[jcr:uuid] in(cast('1' as string), cast('2' as string))) */ - inner join [nt:base] as [b] /* property jcr:uuid + inner join [nt:base] as [b] /* property usedIndex[jcr:uuid] where [b].[jcr:uuid] is not null */ on isdescendantnode([b], [a]) explain select * from [nt:base] where length([jcr:uuid])=1 or upper([jcr:uuid])='1' or lower([jcr:uuid])='3' -[nt:base] as [nt:base] /* property jcr:uuid +[nt:base] as [nt:base] /* property usedIndex[jcr:uuid] where [nt:base].[jcr:uuid] is not null */ explain select * from [nt:base] where [jcr:uuid] = '1' or ([jcr:uuid] = '2' and [b] = '3') -[nt:base] as [nt:base] /* property jcr:uuid +[nt:base] as [nt:base] /* property usedIndex[jcr:uuid] where ([nt:base].[jcr:uuid] is not null) and ([nt:base].[jcr:uuid] in(cast('1' as string), cast('2' as string))) */ explain select * from [nt:base] where [jcr:uuid] in('1', '2') -[nt:base] as [nt:base] /* property jcr:uuid +[nt:base] as [nt:base] /* property usedIndex[jcr:uuid] where [nt:base].[jcr:uuid] in(cast('1' as string), cast('2' as string)) */ explain select * from [nt:base] where [jcr:uuid] = '1' or [jcr:uuid] = '2' -[nt:base] as [nt:base] /* property jcr:uuid +[nt:base] as [nt:base] /* property usedIndex[jcr:uuid] where ([nt:base].[jcr:uuid] is not null) and ([nt:base].[jcr:uuid] in(cast('1' as string), cast('2' as string))) */ explain select * from [nt:base] where [jcr:uuid] = '123' -[nt:base] as [nt:base] /* property jcr:uuid=123 +[nt:base] as [nt:base] /* property usedIndex[jcr:uuid=123] where [nt:base].[jcr:uuid] = cast('123' as string) */ explain select * from [nt:base] where [jcr:uuid] is not null -[nt:base] as [nt:base] /* property jcr:uuid +[nt:base] as [nt:base] /* property usedIndex[jcr:uuid] where [nt:base].[jcr:uuid] is not null */ explain select * @@ -179,9 +179,9 @@ explain select * inner join [nt:base] as b on isdescendantnode(b, a) where a.[jcr:uuid] is not null and b.[jcr:uuid] is not null -[nt:base] as [a] /* property jcr:uuid +[nt:base] as [a] /* property usedIndex[jcr:uuid] where [a].[jcr:uuid] is not null */ - inner join [nt:base] as [b] /* property jcr:uuid + inner join [nt:base] as [b] /* property usedIndex[jcr:uuid] where [b].[jcr:uuid] is not null */ on isdescendantnode([b], [a]) @@ -190,10 +190,10 @@ explain select * inner join [nt:base] as b on isdescendantnode(b, a) where (a.[jcr:uuid]=1 or a.[jcr:uuid]=2) and (b.[jcr:uuid]=3 or b.[jcr:uuid]=4) -[nt:base] as [a] /* property jcr:uuid +[nt:base] as [a] /* property usedIndex[jcr:uuid] where ([a].[jcr:uuid] is not null) and ([a].[jcr:uuid] in(cast('1' as long), cast('2' as long))) */ - inner join [nt:base] as [b] /* property jcr:uuid + inner join [nt:base] as [b] /* property usedIndex[jcr:uuid] where ([b].[jcr:uuid] is not null) and ([b].[jcr:uuid] in(cast('3' as long), cast('4' as long))) */ on isdescendantnode([b], [a]) @@ -203,7 +203,7 @@ explain select * inner join [nt:base] as b on isdescendantnode(b, a) where a.[jcr:uuid] is not null and b.[x] is not null -[nt:base] as [a] /* property jcr:uuid +[nt:base] as [a] /* property usedIndex[jcr:uuid] where [a].[jcr:uuid] is not null */ inner join [nt:base] as [b] /* traverse "* && //path/from/join//*" where [b].[x] is not null */ @@ -212,7 +212,7 @@ explain select * explain select [rep:excerpt] from [nt:base] where [jcr:uuid] is not null -[nt:base] as [nt:base] /* property jcr:uuid (rep:excerpt) +[nt:base] as [nt:base] /* property usedIndex[jcr:uuid] notIndexed[rep:excerpt] where [nt:base].[jcr:uuid] is not null */ commit / + "test": { "jcr:uuid": "xyz", "a": { "jcr:uuid": "123" } } @@ -248,3 +248,30 @@ select [jcr:path] /test commit / - "test" + +commit / + "test": { "jcr:uuid": "123", "indexedProperty1": "bar" } +commit / + "test2": { "jcr:uuid": "456", "indexedProperty1": "bar" } +commit / + "test3": { "jcr:uuid": "789", "indexedProperty1": "bar" } +commit / + "test4": { "jcr:uuid": "012", "indexedProperty1": "baz" } +commit / + "test5": { "jcr:uuid": "345", "indexedProperty1": "baz", "nonIndexProperty": "foo" } + +explain select * + from [nt:base] + where [jcr:uuid] = "123" and [indexedProperty1] = "bar" +[nt:base] as [nt:base] /* property unusedIndex[indexedProperty1=bar] usedIndex[jcr:uuid=123] + where ([nt:base].[jcr:uuid] = cast('123' as string)) + and ([nt:base].[indexedProperty1] = cast('bar' as string)) */ + +explain select * + from [nt:base] + where [jcr:uuid] is not null and [indexedProperty1] = "baz" and nonIndexedProperty = "foo" +[nt:base] as [nt:base] /* property usedIndex[indexedProperty1=baz] unusedIndex[jcr:uuid] notIndexed[nonIndexedProperty=foo] + where (([nt:base].[jcr:uuid] is not null) + and ([nt:base].[indexedProperty1] = cast('baz' as string))) + and ([nt:base].[nonIndexedProperty] = cast('foo' as string)) */ + +commit / - "test" +commit / - "test2" +commit / - "test3" +commit / - "test4" +commit / - "test5"