Skip to content

Commit dfa2231

Browse files
Optimize query execution by deferring query evaluation (#3201)
1 parent 1986969 commit dfa2231

File tree

9 files changed

+102
-138
lines changed

9 files changed

+102
-138
lines changed

firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalDocumentsView.java

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -254,26 +254,21 @@ private ImmutableSortedMap<DocumentKey, Document> getDocumentsMatchingCollection
254254
/** Queries the remote documents and overlays by doing a full collection scan. */
255255
private ImmutableSortedMap<DocumentKey, Document> getDocumentsMatchingCollectionQuery(
256256
Query query, IndexOffset offset) {
257-
ImmutableSortedMap<DocumentKey, MutableDocument> remoteDocuments =
258-
remoteDocumentCache.getAllDocumentsMatchingQuery(query, offset);
257+
Map<DocumentKey, MutableDocument> remoteDocuments =
258+
remoteDocumentCache.getAll(query.getPath(), offset);
259259
Map<DocumentKey, Mutation> overlays = documentOverlayCache.getOverlays(query.getPath(), -1);
260260

261-
// As documents might match the query because of their overlay we need to include all documents
262-
// in the result.
263-
Set<DocumentKey> missingDocuments = new HashSet<>();
261+
// As documents might match the query because of their overlay we need to include documents
262+
// for all overlays in the initial document set.
264263
for (Map.Entry<DocumentKey, Mutation> entry : overlays.entrySet()) {
265264
if (!remoteDocuments.containsKey(entry.getKey())) {
266-
missingDocuments.add(entry.getKey());
265+
remoteDocuments.put(entry.getKey(), MutableDocument.newInvalidDocument(entry.getKey()));
267266
}
268267
}
269-
for (Map.Entry<DocumentKey, MutableDocument> entry :
270-
remoteDocumentCache.getAll(missingDocuments).entrySet()) {
271-
remoteDocuments = remoteDocuments.insert(entry.getKey(), entry.getValue());
272-
}
273268

274269
// Apply the overlays and match against the query.
275270
ImmutableSortedMap<DocumentKey, Document> results = emptyDocumentMap();
276-
for (Map.Entry<DocumentKey, MutableDocument> docEntry : remoteDocuments) {
271+
for (Map.Entry<DocumentKey, MutableDocument> docEntry : remoteDocuments.entrySet()) {
277272
Mutation overlay = overlays.get(docEntry.getKey());
278273
if (overlay != null) {
279274
overlay.applyToLocalView(docEntry.getValue(), null, Timestamp.now());

firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryRemoteDocumentCache.java

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,10 @@
1414

1515
package com.google.firebase.firestore.local;
1616

17-
import static com.google.firebase.firestore.model.DocumentCollections.emptyMutableDocumentMap;
1817
import static com.google.firebase.firestore.util.Assert.hardAssert;
1918

2019
import androidx.annotation.NonNull;
2120
import com.google.firebase.database.collection.ImmutableSortedMap;
22-
import com.google.firebase.firestore.core.Query;
2321
import com.google.firebase.firestore.model.DocumentKey;
2422
import com.google.firebase.firestore.model.FieldIndex.IndexOffset;
2523
import com.google.firebase.firestore.model.MutableDocument;
@@ -89,41 +87,35 @@ public Map<DocumentKey, MutableDocument> getAll(
8987
}
9088

9189
@Override
92-
public ImmutableSortedMap<DocumentKey, MutableDocument> getAllDocumentsMatchingQuery(
93-
Query query, IndexOffset offset) {
94-
hardAssert(
95-
!query.isCollectionGroupQuery(),
96-
"CollectionGroup queries should be handled in LocalDocumentsView");
97-
ImmutableSortedMap<DocumentKey, MutableDocument> result = emptyMutableDocumentMap();
90+
public Map<DocumentKey, MutableDocument> getAll(ResourcePath collection, IndexOffset offset) {
91+
Map<DocumentKey, MutableDocument> result = new HashMap<>();
9892

9993
// Documents are ordered by key, so we can use a prefix scan to narrow down the documents
10094
// we need to match the query against.
101-
ResourcePath queryPath = query.getPath();
102-
DocumentKey prefix = DocumentKey.fromPath(queryPath.append(""));
95+
DocumentKey prefix = DocumentKey.fromPath(collection.append(""));
10396
Iterator<Map.Entry<DocumentKey, MutableDocument>> iterator = docs.iteratorFrom(prefix);
10497

10598
while (iterator.hasNext()) {
10699
Map.Entry<DocumentKey, MutableDocument> entry = iterator.next();
100+
MutableDocument doc = entry.getValue();
107101

108102
DocumentKey key = entry.getKey();
109-
if (!queryPath.isPrefixOf(key.getPath())) {
103+
if (!collection.isPrefixOf(key.getPath())) {
104+
// We are now scanning the next collection. Abort.
110105
break;
111106
}
112107

113-
MutableDocument doc = entry.getValue();
114-
if (!doc.isFoundDocument()) {
108+
if (key.getPath().length() > collection.length() + 1) {
109+
// Exclude entries from subcollections.
115110
continue;
116111
}
117112

118113
if (IndexOffset.fromDocument(doc).compareTo(offset) <= 0) {
114+
// The document sorts before the offset.
119115
continue;
120116
}
121117

122-
if (!query.matches(doc)) {
123-
continue;
124-
}
125-
126-
result = result.insert(doc.getKey(), doc.clone());
118+
result.put(doc.getKey(), doc.clone());
127119
}
128120

129121
return result;

firebase-firestore/src/main/java/com/google/firebase/firestore/local/RemoteDocumentCache.java

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,10 @@
1414

1515
package com.google.firebase.firestore.local;
1616

17-
import com.google.firebase.database.collection.ImmutableSortedMap;
18-
import com.google.firebase.firestore.core.Query;
1917
import com.google.firebase.firestore.model.DocumentKey;
2018
import com.google.firebase.firestore.model.FieldIndex.IndexOffset;
2119
import com.google.firebase.firestore.model.MutableDocument;
20+
import com.google.firebase.firestore.model.ResourcePath;
2221
import com.google.firebase.firestore.model.SnapshotVersion;
2322
import java.util.Map;
2423

@@ -72,24 +71,18 @@ interface RemoteDocumentCache {
7271
* @param collectionGroup The collection group to scan.
7372
* @param offset The offset to start the scan at.
7473
* @param limit The maximum number of results to return.
75-
* @return A map with next set of documents.
74+
* @return A newly created map with next set of documents.
7675
*/
7776
Map<DocumentKey, MutableDocument> getAll(String collectionGroup, IndexOffset offset, int limit);
7877

7978
/**
80-
* Executes a query against the cached Document entries
79+
* Returns the documents from the provided collection.
8180
*
82-
* <p>Implementations may return extra documents if convenient. The results should be re-filtered
83-
* by the consumer before presenting them to the user.
84-
*
85-
* <p>Cached entries for non-existing documents have no bearing on query results.
86-
*
87-
* @param query The query to match documents against.
81+
* @param collection The collection to read.
8882
* @param offset The read time and document key to start scanning at (exclusive).
89-
* @return The set of matching documents.
83+
* @return A newly created map with the set of documents in the collection.
9084
*/
91-
ImmutableSortedMap<DocumentKey, MutableDocument> getAllDocumentsMatchingQuery(
92-
Query query, IndexOffset offset);
85+
Map<DocumentKey, MutableDocument> getAll(ResourcePath collection, IndexOffset offset);
9386

9487
/** Returns the latest read time of any document in the cache. */
9588
SnapshotVersion getLatestReadTime();

firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteRemoteDocumentCache.java

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,6 @@
2121

2222
import androidx.annotation.VisibleForTesting;
2323
import com.google.firebase.Timestamp;
24-
import com.google.firebase.database.collection.ImmutableSortedMap;
25-
import com.google.firebase.firestore.core.Query;
26-
import com.google.firebase.firestore.model.DocumentCollections;
2724
import com.google.firebase.firestore.model.DocumentKey;
2825
import com.google.firebase.firestore.model.FieldIndex.IndexOffset;
2926
import com.google.firebase.firestore.model.MutableDocument;
@@ -234,23 +231,8 @@ private Map<DocumentKey, MutableDocument> getAll(
234231
}
235232

236233
@Override
237-
public ImmutableSortedMap<DocumentKey, MutableDocument> getAllDocumentsMatchingQuery(
238-
final Query query, IndexOffset offset) {
239-
hardAssert(
240-
!query.isCollectionGroupQuery(),
241-
"CollectionGroup queries should be handled in LocalDocumentsView");
242-
243-
Map<DocumentKey, MutableDocument> allDocuments =
244-
getAll(Collections.singletonList(query.getPath()), offset, Integer.MAX_VALUE);
245-
246-
ImmutableSortedMap<DocumentKey, MutableDocument> matchingDocuments =
247-
DocumentCollections.emptyMutableDocumentMap();
248-
for (MutableDocument document : allDocuments.values()) {
249-
if (document.isFoundDocument() && query.matches(document)) {
250-
matchingDocuments = matchingDocuments.insert(document.getKey(), document);
251-
}
252-
}
253-
return matchingDocuments;
234+
public Map<DocumentKey, MutableDocument> getAll(ResourcePath collection, IndexOffset offset) {
235+
return getAll(Collections.singletonList(collection), offset, Integer.MAX_VALUE);
254236
}
255237

256238
@Override

firebase-firestore/src/main/java/com/google/firebase/firestore/model/MutableDocument.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,22 +62,25 @@ private enum DocumentState {
6262
private final DocumentKey key;
6363
private DocumentType documentType;
6464
private SnapshotVersion version;
65-
private SnapshotVersion readTime = SnapshotVersion.NONE;
65+
private SnapshotVersion readTime;
6666
private ObjectValue value;
6767
private DocumentState documentState;
6868

6969
private MutableDocument(DocumentKey key) {
7070
this.key = key;
71+
this.readTime = SnapshotVersion.NONE;
7172
}
7273

7374
private MutableDocument(
7475
DocumentKey key,
7576
DocumentType documentType,
7677
SnapshotVersion version,
78+
SnapshotVersion readTime,
7779
ObjectValue value,
7880
DocumentState documentState) {
7981
this.key = key;
8082
this.version = version;
83+
this.readTime = readTime;
8184
this.documentType = documentType;
8285
this.documentState = documentState;
8386
this.value = value;
@@ -92,6 +95,7 @@ public static MutableDocument newInvalidDocument(DocumentKey documentKey) {
9295
documentKey,
9396
DocumentType.INVALID,
9497
SnapshotVersion.NONE,
98+
SnapshotVersion.NONE,
9599
new ObjectValue(),
96100
DocumentState.SYNCED);
97101
}
@@ -226,7 +230,7 @@ public boolean isUnknownDocument() {
226230
@Override
227231
@NonNull
228232
public MutableDocument clone() {
229-
return new MutableDocument(key, documentType, version, value.clone(), documentState);
233+
return new MutableDocument(key, documentType, version, readTime, value.clone(), documentState);
230234
}
231235

232236
@Override
@@ -238,6 +242,8 @@ public boolean equals(Object o) {
238242

239243
if (!key.equals(document.key)) return false;
240244
if (!version.equals(document.version)) return false;
245+
// TODO(mrschmidt): Include readTime (requires a lot of test updates)
246+
// if (!readTime.equals(document.readTime)) return false;
241247
if (!documentType.equals(document.documentType)) return false;
242248
if (!documentState.equals(document.documentState)) return false;
243249
return value.equals(document.value);

firebase-firestore/src/test/java/com/google/firebase/firestore/local/CountingQueryEngine.java

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.google.firebase.firestore.model.DocumentKey;
2424
import com.google.firebase.firestore.model.FieldIndex.IndexOffset;
2525
import com.google.firebase.firestore.model.MutableDocument;
26+
import com.google.firebase.firestore.model.ResourcePath;
2627
import com.google.firebase.firestore.model.SnapshotVersion;
2728
import com.google.firebase.firestore.model.mutation.Mutation;
2829
import com.google.firebase.firestore.model.mutation.MutationBatch;
@@ -37,19 +38,19 @@
3738
class CountingQueryEngine implements QueryEngine {
3839
private final QueryEngine queryEngine;
3940

40-
private final int[] mutationsReadByQuery = new int[] {0};
41+
private final int[] mutationsReadByCollection = new int[] {0};
4142
private final int[] mutationsReadByKey = new int[] {0};
42-
private final int[] documentsReadByQuery = new int[] {0};
43+
private final int[] documentsReadByCollection = new int[] {0};
4344
private final int[] documentsReadByKey = new int[] {0};
4445

4546
CountingQueryEngine(QueryEngine queryEngine) {
4647
this.queryEngine = queryEngine;
4748
}
4849

4950
void resetCounts() {
50-
mutationsReadByQuery[0] = 0;
51+
mutationsReadByCollection[0] = 0;
5152
mutationsReadByKey[0] = 0;
52-
documentsReadByQuery[0] = 0;
53+
documentsReadByCollection[0] = 0;
5354
documentsReadByKey[0] = 0;
5455
}
5556

@@ -83,11 +84,11 @@ QueryEngine getSubject() {
8384
}
8485

8586
/**
86-
* Returns the number of documents returned by the RemoteDocumentCache's
87-
* `getDocumentsMatchingQuery()` API (since the last call to `resetCounts()`)
87+
* Returns the number of documents returned by the RemoteDocumentCache's `getAll()` API (since the
88+
* last call to `resetCounts()`)
8889
*/
89-
int getDocumentsReadByQuery() {
90-
return documentsReadByQuery[0];
90+
int getDocumentsReadByCollection() {
91+
return documentsReadByCollection[0];
9192
}
9293

9394
/**
@@ -102,8 +103,8 @@ int getDocumentsReadByKey() {
102103
* Returns the number of mutations returned by the MutationQueue's
103104
* `getAllMutationBatchesAffectingQuery()` API (since the last call to `resetCounts()`)
104105
*/
105-
int getMutationsReadByQuery() {
106-
return mutationsReadByQuery[0];
106+
int getMutationsReadByCollection() {
107+
return mutationsReadByCollection[0];
107108
}
108109

109110
/**
@@ -153,16 +154,15 @@ public Map<DocumentKey, MutableDocument> getAll(Iterable<DocumentKey> documentKe
153154
public Map<DocumentKey, MutableDocument> getAll(
154155
String collectionGroup, IndexOffset offset, int limit) {
155156
Map<DocumentKey, MutableDocument> result = subject.getAll(collectionGroup, offset, limit);
156-
documentsReadByQuery[0] += result.size();
157+
documentsReadByCollection[0] += result.size();
158+
157159
return result;
158160
}
159161

160162
@Override
161-
public ImmutableSortedMap<DocumentKey, MutableDocument> getAllDocumentsMatchingQuery(
162-
Query query, IndexOffset offset) {
163-
ImmutableSortedMap<DocumentKey, MutableDocument> result =
164-
subject.getAllDocumentsMatchingQuery(query, offset);
165-
documentsReadByQuery[0] += result.size();
163+
public Map<DocumentKey, MutableDocument> getAll(ResourcePath collection, IndexOffset offset) {
164+
Map<DocumentKey, MutableDocument> result = subject.getAll(collection, offset);
165+
documentsReadByCollection[0] += result.size();
166166
return result;
167167
}
168168

@@ -250,7 +250,7 @@ public List<MutationBatch> getAllMutationBatchesAffectingDocumentKeys(
250250
@Override
251251
public List<MutationBatch> getAllMutationBatchesAffectingQuery(Query query) {
252252
List<MutationBatch> result = subject.getAllMutationBatchesAffectingQuery(query);
253-
mutationsReadByQuery[0] += result.size();
253+
mutationsReadByCollection[0] += result.size();
254254
return result;
255255
}
256256

firebase-firestore/src/test/java/com/google/firebase/firestore/local/LocalStoreTestCase.java

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -321,18 +321,21 @@ private void assertHasNamedQuery(NamedQuery expectedNamedQuery) {
321321
* Asserts the expected numbers of mutations read by the MutationQueue since the last call to
322322
* `resetPersistenceStats()`.
323323
*/
324-
private void assertMutationsRead(int byKey, int byQuery) {
325-
assertEquals("Mutations read (by query)", byQuery, queryEngine.getMutationsReadByQuery());
324+
private void assertMutationsRead(int byKey, int byCollection) {
325+
assertEquals(
326+
"Mutations read (by collection)", byCollection, queryEngine.getMutationsReadByCollection());
326327
assertEquals("Mutations read (by key)", byKey, queryEngine.getMutationsReadByKey());
327328
}
328329

329330
/**
330331
* Asserts the expected numbers of documents read by the RemoteDocumentCache since the last call
331332
* to `resetPersistenceStats()`.
332333
*/
333-
private void assertRemoteDocumentsRead(int byKey, int byQuery) {
334+
private void assertRemoteDocumentsRead(int byKey, int byCollection) {
334335
assertEquals(
335-
"Remote documents read (by query)", byQuery, queryEngine.getDocumentsReadByQuery());
336+
"Remote documents read (by collection)",
337+
byCollection,
338+
queryEngine.getDocumentsReadByCollection());
336339
assertEquals("Remote documents read (by key)", byKey, queryEngine.getDocumentsReadByKey());
337340
}
338341

@@ -976,10 +979,9 @@ public void testReadsAllDocumentsForInitialCollectionQueries() {
976979
resetPersistenceStats();
977980

978981
localStore.executeQuery(query, /* usePreviousResults= */ true);
979-
980-
assertRemoteDocumentsRead(/* byKey= */ 0, /* byQuery= */ 2);
982+
assertRemoteDocumentsRead(/* byKey= */ 0, /* byCollection= */ 2);
981983
// No mutations are read because only overlay is needed.
982-
assertMutationsRead(/* byKey= */ 0, /* byQuery= */ 0);
984+
assertMutationsRead(/* byKey= */ 0, /* byCollection= */ 0);
983985
}
984986

985987
@Test
@@ -1104,7 +1106,7 @@ public void testUsesTargetMappingToExecuteQueries() {
11041106
// Execute the query, but note that we read all existing documents from the RemoteDocumentCache
11051107
// since we do not yet have target mapping.
11061108
executeQuery(query);
1107-
assertRemoteDocumentsRead(/* byKey= */ 0, /* byQuery= */ 2);
1109+
assertRemoteDocumentsRead(/* byKey= */ 0, /* byCollection= */ 3);
11081110

11091111
// Issue a RemoteEvent to persist the target mapping.
11101112
applyRemoteEvent(
@@ -1118,7 +1120,7 @@ public void testUsesTargetMappingToExecuteQueries() {
11181120
// Execute the query again, this time verifying that we only read the two documents that match
11191121
// the query.
11201122
executeQuery(query);
1121-
assertRemoteDocumentsRead(/* byKey= */ 2, /* byQuery= */ 0);
1123+
assertRemoteDocumentsRead(/* byKey= */ 2, /* byCollection= */ 0);
11221124
assertQueryReturned("foo/a", "foo/b");
11231125
}
11241126

0 commit comments

Comments
 (0)