Skip to content

Commit 9466242

Browse files
Use path length to skip scanning subcollections (#3192)
1 parent 9e5c8e1 commit 9466242

File tree

13 files changed

+198
-104
lines changed

13 files changed

+198
-104
lines changed

firebase-firestore/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ by opting into a release at
33
[go/firebase-android-release](http:go/firebase-android-release) (Googlers only).
44

55
# Unreleased
6+
- [changed] Improved performance for queries with collections that contain
7+
subcollections.
8+
9+
# 24.0.0
610
- [feature] Added support for Firebase AppCheck.
711

812
# 23.0.4

firebase-firestore/src/androidTest/java/com/google/firebase/firestore/ConformanceTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,6 @@ private com.google.firebase.firestore.FieldPath formatFieldPath(String serverFor
284284
}
285285

286286
private String getId(Document document) {
287-
return DocumentKey.fromName(document.getName()).getPath().getLastSegment();
287+
return DocumentKey.fromName(document.getName()).getDocumentId();
288288
}
289289
}

firebase-firestore/src/androidTest/java/com/google/firebase/firestore/conformance/TestCaseConverter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ private List<Collection> convertDatabaseContents(DatastoreTestTrace.FirestoreV1A
4848
if (response.hasDocument()) {
4949
Document document = response.getDocument();
5050
DocumentKey documentKey = DocumentKey.fromName(document.getName());
51-
String collectionId = documentKey.getPath().popLast().getLastSegment();
51+
String collectionId = documentKey.getCollectionGroup();
5252
List<Document> documents =
5353
collections.computeIfAbsent(collectionId, name -> new ArrayList<>());
5454
documents.add(document);

firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentReference.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ public FirebaseFirestore getFirestore() {
9696

9797
@NonNull
9898
public String getId() {
99-
return key.getPath().getLastSegment();
99+
return key.getDocumentId();
100100
}
101101

102102
/**
@@ -106,7 +106,7 @@ public String getId() {
106106
*/
107107
@NonNull
108108
public CollectionReference getParent() {
109-
return new CollectionReference(key.getPath().popLast(), firestore);
109+
return new CollectionReference(key.getCollectionPath(), firestore);
110110
}
111111

112112
/**

firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentSnapshot.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ static DocumentSnapshot fromNoDocument(
102102
/** @return The id of the document. */
103103
@NonNull
104104
public String getId() {
105-
return key.getPath().getLastSegment();
105+
return key.getDocumentId();
106106
}
107107

108108
/** @return The metadata for this document snapshot. */

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ public MutationBatch addMutationBatch(
152152
batchesByDocumentKey =
153153
batchesByDocumentKey.insert(new DocumentReference(mutation.getKey(), batchId));
154154

155-
indexManager.addToCollectionParentIndex(mutation.getKey().getPath().popLast());
155+
indexManager.addToCollectionParentIndex(mutation.getKey().getCollectionPath());
156156
}
157157

158158
return batch;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public void add(MutableDocument document, SnapshotVersion readTime) {
5858
docs = docs.insert(document.getKey(), document.clone().withReadTime(readTime));
5959
latestReadTime = readTime.compareTo(latestReadTime) > 0 ? readTime : latestReadTime;
6060

61-
indexManager.addToCollectionParentIndex(document.getKey().getPath().popLast());
61+
indexManager.addToCollectionParentIndex(document.getKey().getCollectionPath());
6262
}
6363

6464
@Override

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ public MutationBatch addMutationBatch(
215215
String path = EncodedPath.encode(key.getPath());
216216
db.execute(indexInserter, uid, path, batchId);
217217

218-
indexManager.addToCollectionParentIndex(key.getPath().popLast());
218+
indexManager.addToCollectionParentIndex(key.getCollectionPath());
219219
}
220220

221221
return batch;

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

Lines changed: 66 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import com.google.firebase.firestore.model.DocumentKey;
2525
import com.google.firebase.firestore.model.FieldIndex;
2626
import com.google.firebase.firestore.model.MutableDocument;
27-
import com.google.firebase.firestore.model.ResourcePath;
2827
import com.google.firebase.firestore.model.SnapshotVersion;
2928
import com.google.firebase.firestore.util.BackgroundQueue;
3029
import com.google.firebase.firestore.util.Executors;
@@ -58,52 +57,50 @@ public void add(MutableDocument document, SnapshotVersion readTime) {
5857
!readTime.equals(SnapshotVersion.NONE),
5958
"Cannot add document to the RemoteDocumentCache with a read time of zero");
6059

61-
String path = pathForKey(document.getKey());
60+
DocumentKey documentKey = document.getKey();
6261
Timestamp timestamp = readTime.getTimestamp();
6362
MessageLite message = serializer.encodeMaybeDocument(document);
6463

6564
db.execute(
6665
"INSERT OR REPLACE INTO remote_documents "
67-
+ "(path, read_time_seconds, read_time_nanos, contents) "
68-
+ "VALUES (?, ?, ?, ?)",
69-
path,
66+
+ "(path, path_length, read_time_seconds, read_time_nanos, contents) "
67+
+ "VALUES (?, ?, ?, ?, ?)",
68+
EncodedPath.encode(documentKey.getPath()),
69+
documentKey.getPath().length(),
7070
timestamp.getSeconds(),
7171
timestamp.getNanoseconds(),
7272
message.toByteArray());
7373

74-
indexManager.addToCollectionParentIndex(document.getKey().getPath().popLast());
74+
indexManager.addToCollectionParentIndex(document.getKey().getCollectionPath());
7575
}
7676

7777
@Override
7878
public void remove(DocumentKey documentKey) {
79-
String path = pathForKey(documentKey);
79+
String path = EncodedPath.encode(documentKey.getPath());
8080

8181
db.execute("DELETE FROM remote_documents WHERE path = ?", path);
8282
}
8383

8484
@Override
8585
public MutableDocument get(DocumentKey documentKey) {
86-
String path = pathForKey(documentKey);
86+
String path = EncodedPath.encode(documentKey.getPath());
8787

8888
MutableDocument document =
8989
db.query(
9090
"SELECT contents, read_time_seconds, read_time_nanos "
91-
+ "FROM remote_documents "
92-
+ "WHERE path = ?")
91+
+ "FROM remote_documents WHERE path = ?")
9392
.binding(path)
9493
.firstValue(row -> decodeMaybeDocument(row.getBlob(0), row.getInt(1), row.getInt(2)));
9594
return document != null ? document : MutableDocument.newInvalidDocument(documentKey);
9695
}
9796

9897
@Override
9998
public Map<DocumentKey, MutableDocument> getAll(Iterable<DocumentKey> documentKeys) {
100-
List<Object> args = new ArrayList<>();
101-
for (DocumentKey key : documentKeys) {
102-
args.add(EncodedPath.encode(key.getPath()));
103-
}
104-
10599
Map<DocumentKey, MutableDocument> results = new HashMap<>();
100+
List<Object> bindVars = new ArrayList<>();
106101
for (DocumentKey key : documentKeys) {
102+
bindVars.add(EncodedPath.encode(key.getPath()));
103+
107104
// Make sure each key has a corresponding entry, which is null in case the document is not
108105
// found.
109106
results.put(key, MutableDocument.newInvalidDocument(key));
@@ -114,7 +111,7 @@ public Map<DocumentKey, MutableDocument> getAll(Iterable<DocumentKey> documentKe
114111
db,
115112
"SELECT contents, read_time_seconds, read_time_nanos FROM remote_documents "
116113
+ "WHERE path IN (",
117-
args,
114+
bindVars,
118115
") ORDER BY path");
119116

120117
while (longQuery.hasMoreSubqueries()) {
@@ -138,88 +135,74 @@ public ImmutableSortedMap<DocumentKey, MutableDocument> getAllDocumentsMatchingQ
138135
!query.isCollectionGroupQuery(),
139136
"CollectionGroup queries should be handled in LocalDocumentsView");
140137

141-
// Use the query path as a prefix for testing if a document matches the query.
142-
ResourcePath prefix = query.getPath();
143-
int immediateChildrenPathLength = prefix.length() + 1;
138+
StringBuilder sql =
139+
new StringBuilder(
140+
"SELECT contents, read_time_seconds, read_time_nanos "
141+
+ "FROM remote_documents WHERE path >= ? AND path < ? AND path_length = ?");
144142

145-
String prefixPath = EncodedPath.encode(prefix);
146-
String prefixSuccessorPath = EncodedPath.prefixSuccessor(prefixPath);
143+
boolean hasOffset = !FieldIndex.IndexOffset.NONE.equals(offset);
144+
Object[] bindVars = new Object[3 + (hasOffset ? 6 : 0)];
147145

148-
BackgroundQueue backgroundQueue = new BackgroundQueue();
146+
String prefix = EncodedPath.encode(query.getPath());
149147

150-
ImmutableSortedMap<DocumentKey, MutableDocument>[] matchingDocuments =
151-
(ImmutableSortedMap<DocumentKey, MutableDocument>[])
152-
new ImmutableSortedMap[] {DocumentCollections.emptyMutableDocumentMap()};
148+
int i = 0;
149+
bindVars[i++] = prefix;
150+
bindVars[i++] = EncodedPath.prefixSuccessor(prefix);
151+
bindVars[i++] = query.getPath().length() + 1;
153152

154-
SQLitePersistence.Query sqlQuery;
155-
if (FieldIndex.IndexOffset.NONE.equals(offset)) {
156-
sqlQuery =
157-
db.query(
158-
"SELECT path, contents, read_time_seconds, read_time_nanos "
159-
+ "FROM remote_documents WHERE path >= ? AND path < ?")
160-
.binding(prefixPath, prefixSuccessorPath);
161-
} else {
153+
if (hasOffset) {
162154
Timestamp readTime = offset.getReadTime().getTimestamp();
163155
DocumentKey documentKey = offset.getDocumentKey();
164156

165-
sqlQuery =
166-
db.query(
167-
"SELECT path, contents, read_time_seconds, read_time_nanos "
168-
+ "FROM remote_documents WHERE path >= ? AND path < ? AND ("
169-
+ "read_time_seconds > ? OR ("
170-
+ "read_time_seconds = ? AND read_time_nanos > ?) OR ("
171-
+ "read_time_seconds = ? AND read_time_nanos = ? and path > ?))")
172-
.binding(
173-
prefixPath,
174-
prefixSuccessorPath,
175-
readTime.getSeconds(),
176-
readTime.getSeconds(),
177-
readTime.getNanoseconds(),
178-
readTime.getSeconds(),
179-
readTime.getNanoseconds(),
180-
EncodedPath.encode(documentKey.getPath()));
157+
sql.append(
158+
" AND (read_time_seconds > ? OR ("
159+
+ "read_time_seconds = ? AND read_time_nanos > ?) OR ("
160+
+ "read_time_seconds = ? AND read_time_nanos = ? and path > ?))");
161+
bindVars[i++] = readTime.getSeconds();
162+
bindVars[i++] = readTime.getSeconds();
163+
bindVars[i++] = readTime.getNanoseconds();
164+
bindVars[i++] = readTime.getSeconds();
165+
bindVars[i++] = readTime.getNanoseconds();
166+
bindVars[i] = EncodedPath.encode(documentKey.getPath());
181167
}
182-
sqlQuery.forEach(
183-
row -> {
184-
// TODO: Actually implement a single-collection query
185-
//
186-
// The query is actually returning any path that starts with the query path prefix
187-
// which may include documents in subcollections. For example, a query on 'rooms'
188-
// will return rooms/abc/messages/xyx but we shouldn't match it. Fix this by
189-
// discarding rows with document keys more than one segment longer than the query
190-
// path.
191-
ResourcePath path = EncodedPath.decodeResourcePath(row.getString(0));
192-
if (path.length() != immediateChildrenPathLength) {
193-
return;
194-
}
195-
196-
// Store row values in array entries to provide the correct context inside the executor.
197-
final byte[] rawDocument = row.getBlob(1);
198-
final int[] readTimeSeconds = {row.getInt(2)};
199-
final int[] readTimeNanos = {row.getInt(3)};
200-
201-
// Since scheduling background tasks incurs overhead, we only dispatch to a
202-
// background thread if there are still some documents remaining.
203-
Executor executor = row.isLast() ? Executors.DIRECT_EXECUTOR : backgroundQueue;
204-
executor.execute(
205-
() -> {
206-
MutableDocument document =
207-
decodeMaybeDocument(rawDocument, readTimeSeconds[0], readTimeNanos[0]);
208-
if (document.isFoundDocument() && query.matches(document)) {
209-
synchronized (SQLiteRemoteDocumentCache.this) {
210-
matchingDocuments[0] = matchingDocuments[0].insert(document.getKey(), document);
211-
}
212-
}
213-
});
214-
});
168+
169+
ImmutableSortedMap<DocumentKey, MutableDocument>[] results =
170+
(ImmutableSortedMap<DocumentKey, MutableDocument>[])
171+
new ImmutableSortedMap[] {DocumentCollections.emptyMutableDocumentMap()};
172+
BackgroundQueue backgroundQueue = new BackgroundQueue();
173+
174+
db.query(sql.toString())
175+
.binding(bindVars)
176+
.forEach(
177+
row -> {
178+
// Store row values in array entries to provide the correct context inside the
179+
// executor.
180+
final byte[] rawDocument = row.getBlob(0);
181+
final int[] readTimeSeconds = {row.getInt(1)};
182+
final int[] readTimeNanos = {row.getInt(2)};
183+
184+
// Since scheduling background tasks incurs overhead, we only dispatch to a
185+
// background thread if there are still some documents remaining.
186+
Executor executor = row.isLast() ? Executors.DIRECT_EXECUTOR : backgroundQueue;
187+
executor.execute(
188+
() -> {
189+
MutableDocument document =
190+
decodeMaybeDocument(rawDocument, readTimeSeconds[0], readTimeNanos[0]);
191+
if (document.isFoundDocument() && query.matches(document)) {
192+
synchronized (SQLiteRemoteDocumentCache.this) {
193+
results[0] = results[0].insert(document.getKey(), document);
194+
}
195+
}
196+
});
197+
});
215198

216199
try {
217200
backgroundQueue.drain();
218201
} catch (InterruptedException e) {
219202
fail("Interrupted while deserializing documents", e);
220203
}
221204

222-
return matchingDocuments[0];
205+
return results[0];
223206
}
224207

225208
@Override
@@ -233,10 +216,6 @@ public SnapshotVersion getLatestReadTime() {
233216
return latestReadTime != null ? latestReadTime : SnapshotVersion.NONE;
234217
}
235218

236-
private String pathForKey(DocumentKey key) {
237-
return EncodedPath.encode(key.getPath());
238-
}
239-
240219
private MutableDocument decodeMaybeDocument(
241220
byte[] bytes, int readTimeSeconds, int readTimeNanos) {
242221
try {

0 commit comments

Comments
 (0)