Skip to content

Optimize query execution by deferring query evaluation #3201

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 29 commits into from
Dec 8, 2021
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
cf225f9
Migrate remote_documents table to use dedicated collection_path column
schmidt-sebastian Nov 24, 2021
7f16305
Add timer
schmidt-sebastian Nov 24, 2021
ba26b87
Add DocumentId helpers
schmidt-sebastian Nov 29, 2021
60a4240
Add one more test
schmidt-sebastian Nov 29, 2021
9a12530
Rename
schmidt-sebastian Nov 29, 2021
dade039
Merge branch 'master' into mrschmidt/newrdc
schmidt-sebastian Nov 29, 2021
07500fc
Format
schmidt-sebastian Nov 29, 2021
7671330
Don't delete column
schmidt-sebastian Nov 29, 2021
b25191f
Changelog
schmidt-sebastian Nov 30, 2021
0f93b23
Merge
schmidt-sebastian Nov 30, 2021
723defc
Use PathLength
schmidt-sebastian Nov 30, 2021
fb15500
Merge branch 'master' into mrschmidt/pathlength
schmidt-sebastian Dec 1, 2021
fc00ff6
Fetch 50 documents
schmidt-sebastian Dec 1, 2021
759a50b
Simplify
schmidt-sebastian Dec 2, 2021
eca7547
Optimize query execution by deferring query evaluation
schmidt-sebastian Dec 2, 2021
acc43e9
Format
schmidt-sebastian Dec 2, 2021
86e35ea
Merge branch 'mrschmidt/getnext' into mrschmidt/dontquery
schmidt-sebastian Dec 2, 2021
c4963a3
Fill read_time
schmidt-sebastian Dec 2, 2021
ac17fac
Merge branch 'mrschmidt/getnext' into mrschmidt/dontquery
schmidt-sebastian Dec 2, 2021
0f56227
Review
schmidt-sebastian Dec 3, 2021
cf107bd
Merge
schmidt-sebastian Dec 3, 2021
f6f3664
Review
schmidt-sebastian Dec 3, 2021
fa3483b
Merge
schmidt-sebastian Dec 7, 2021
473e8f4
Format
schmidt-sebastian Dec 7, 2021
4719d11
Review
schmidt-sebastian Dec 8, 2021
a3a03b1
Review
schmidt-sebastian Dec 8, 2021
93b66e5
Merge
schmidt-sebastian Dec 8, 2021
2d2faba
Merge
schmidt-sebastian Dec 8, 2021
c72063d
Cleanup
schmidt-sebastian Dec 8, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions firebase-firestore/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ by opting into a release at
[go/firebase-android-release](http:go/firebase-android-release) (Googlers only).

# Unreleased
- [changed] Improved performance for queries with collections that contain
subcollections.

# 24.0.0
- [feature] Added support for Firebase AppCheck.

# 23.0.4
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,6 @@ private com.google.firebase.firestore.FieldPath formatFieldPath(String serverFor
}

private String getId(Document document) {
return DocumentKey.fromName(document.getName()).getPath().getLastSegment();
return DocumentKey.fromName(document.getName()).getDocumentId();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ private List<Collection> convertDatabaseContents(DatastoreTestTrace.FirestoreV1A
if (response.hasDocument()) {
Document document = response.getDocument();
DocumentKey documentKey = DocumentKey.fromName(document.getName());
String collectionId = documentKey.getPath().popLast().getLastSegment();
String collectionId = documentKey.getCollectionGroup();
List<Document> documents =
collections.computeIfAbsent(collectionId, name -> new ArrayList<>());
documents.add(document);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public FirebaseFirestore getFirestore() {

@NonNull
public String getId() {
return key.getPath().getLastSegment();
return key.getDocumentId();
}

/**
Expand All @@ -106,7 +106,7 @@ public String getId() {
*/
@NonNull
public CollectionReference getParent() {
return new CollectionReference(key.getPath().popLast(), firestore);
return new CollectionReference(key.getCollectionPath(), firestore);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ static DocumentSnapshot fromNoDocument(
/** @return The id of the document. */
@NonNull
public String getId() {
return key.getPath().getLastSegment();
return key.getDocumentId();
}

/** @return The metadata for this document snapshot. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,15 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.firebase.database.collection.ImmutableSortedMap;
import com.google.firebase.firestore.core.Query;
import com.google.firebase.firestore.model.Document;
import com.google.firebase.firestore.model.DocumentKey;
import com.google.firebase.firestore.model.FieldIndex;
import com.google.firebase.firestore.model.FieldIndex.IndexOffset;
import com.google.firebase.firestore.model.ResourcePath;
import com.google.firebase.firestore.util.AsyncQueue;
import com.google.firebase.firestore.util.Logger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -138,22 +134,18 @@ private int writeIndexEntries(LocalDocumentsView localDocumentsView) {
/** Writes entries for the fetched field indexes. */
private int writeEntriesForCollectionGroup(
LocalDocumentsView localDocumentsView, String collectionGroup, int entriesRemainingUnderCap) {
Query query = new Query(ResourcePath.EMPTY, collectionGroup);
// TODO(indexing): Support mutation batch Ids when sorting and writing indexes.

// Use the earliest offset of all field indexes to query the local cache.
IndexOffset existingOffset = getExistingOffset(indexManager.getFieldIndexes(collectionGroup));

// TODO(indexing): Use limit queries to only fetch the required number of entries.
// TODO(indexing): Support mutation batch Ids when sorting and writing indexes.
ImmutableSortedMap<DocumentKey, Document> documents =
localDocumentsView.getDocumentsMatchingQuery(query, existingOffset);

List<Document> oldestDocuments = getOldestDocuments(documents, entriesRemainingUnderCap);
indexManager.updateIndexEntries(oldestDocuments);
localDocumentsView.getDocuments(collectionGroup, existingOffset, entriesRemainingUnderCap);
indexManager.updateIndexEntries(documents);

IndexOffset newOffset = getNewOffset(oldestDocuments, existingOffset);
IndexOffset newOffset = getNewOffset(documents, existingOffset);
indexManager.updateCollectionGroup(collectionGroup, newOffset);
return oldestDocuments.size();

return documents.size();
}

/** Returns the lowest offset for the provided index group. */
Expand All @@ -168,37 +160,22 @@ private IndexOffset getExistingOffset(Collection<FieldIndex> fieldIndexes) {
return lowestOffset == null ? IndexOffset.NONE : lowestOffset;
}

/**
* Returns the offset for the index based on the newly indexed documents.
*
* @param documents a list of documents sorted by read time and key (ascending)
* @param currentOffset the current offset of the index group
*/
private IndexOffset getNewOffset(List<Document> documents, IndexOffset currentOffset) {
IndexOffset latestOffset =
documents.isEmpty()
? IndexOffset.create(remoteDocumentCache.getLatestReadTime())
: IndexOffset.create(
documents.get(documents.size() - 1).getReadTime(),
documents.get(documents.size() - 1).getKey());
// Make sure the index does not go back in time
latestOffset = latestOffset.compareTo(currentOffset) > 0 ? latestOffset : currentOffset;
return latestOffset;
}

/** Returns up to {@code count} documents sorted by read time and key. */
private List<Document> getOldestDocuments(
ImmutableSortedMap<DocumentKey, Document> documents, int count) {
List<Document> oldestDocuments = new ArrayList<>();
for (Map.Entry<DocumentKey, Document> entry : documents) {
oldestDocuments.add(entry.getValue());
/** Returns the offset for the index based on the newly indexed documents. */
private IndexOffset getNewOffset(
ImmutableSortedMap<DocumentKey, Document> documents, IndexOffset currentOffset) {
if (documents.isEmpty()) {
return IndexOffset.create(remoteDocumentCache.getLatestReadTime());
} else {
IndexOffset latestOffset = currentOffset;
Iterator<Map.Entry<DocumentKey, Document>> it = documents.iterator();
while (it.hasNext()) {
IndexOffset newOffset = IndexOffset.fromDocument(it.next().getValue());
if (newOffset.compareTo(latestOffset) > 0) {
latestOffset = newOffset;
}
}
return latestOffset;
}
Collections.sort(
oldestDocuments,
(l, r) ->
IndexOffset.create(l.getReadTime(), l.getKey())
.compareTo(IndexOffset.create(r.getReadTime(), r.getKey())));
return oldestDocuments.subList(0, Math.min(count, oldestDocuments.size()));
}

@VisibleForTesting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package com.google.firebase.firestore.local;

import androidx.annotation.Nullable;
import com.google.firebase.database.collection.ImmutableSortedMap;
import com.google.firebase.firestore.core.Target;
import com.google.firebase.firestore.model.Document;
import com.google.firebase.firestore.model.DocumentKey;
Expand Down Expand Up @@ -97,5 +98,5 @@ public interface IndexManager {
void updateCollectionGroup(String collectionGroup, FieldIndex.IndexOffset offset);

/** Updates the index entries for the provided documents. */
void updateIndexEntries(Collection<Document> documents);
void updateIndexEntries(ImmutableSortedMap<DocumentKey, Document> documents);
}
Original file line number Diff line number Diff line change
Expand Up @@ -237,10 +237,13 @@ void recalculateAndSaveOverlays(Set<DocumentKey> documentKeys) {
recalculateAndSaveOverlays(docs);
}

// TODO: The Querying implementation here should move 100% to the query engines.
// Instead, we should just provide a getCollectionDocuments() method here that return all the
// documents in a given collection so that query engine can do that and then filter in
// memory.
/** Gets the local view of the next {@code count} documents based on their read time. */
ImmutableSortedMap<DocumentKey, Document> getDocuments(
String collectionGroup, IndexOffset offset, int count) {
Map<DocumentKey, MutableDocument> docs =
remoteDocumentCache.getAll(collectionGroup, offset, count);
return getLocalViewOfDocuments(docs, new HashSet<>());
}

/**
* Performs a query against the local view of all documents.
Expand Down Expand Up @@ -319,26 +322,21 @@ private ImmutableSortedMap<DocumentKey, Document> getDocumentsMatchingCollection
/** Queries the remote documents and overlays by doing a full collection scan. */
private ImmutableSortedMap<DocumentKey, Document>
getDocumentsMatchingCollectionQueryFromOverlayCache(Query query, IndexOffset offset) {
ImmutableSortedMap<DocumentKey, MutableDocument> remoteDocuments =
remoteDocumentCache.getAllDocumentsMatchingQuery(query, offset);
Map<DocumentKey, MutableDocument> remoteDocuments =
remoteDocumentCache.getAll(query.getPath(), offset);
Map<DocumentKey, Mutation> overlays = documentOverlayCache.getOverlays(query.getPath(), -1);

// As documents might match the query because of their overlay we need to include all documents
// in the result.
Set<DocumentKey> missingDocuments = new HashSet<>();
// As documents might match the query because of their overlay we need to include documents
// for all overlays in the initial document set.
for (Map.Entry<DocumentKey, Mutation> entry : overlays.entrySet()) {
if (!remoteDocuments.containsKey(entry.getKey())) {
missingDocuments.add(entry.getKey());
remoteDocuments.put(entry.getKey(), MutableDocument.newInvalidDocument(entry.getKey()));
}
}
for (Map.Entry<DocumentKey, MutableDocument> entry :
remoteDocumentCache.getAll(missingDocuments).entrySet()) {
remoteDocuments = remoteDocuments.insert(entry.getKey(), entry.getValue());
}

// Apply the overlays and match against the query.
ImmutableSortedMap<DocumentKey, Document> results = emptyDocumentMap();
for (Map.Entry<DocumentKey, MutableDocument> docEntry : remoteDocuments) {
for (Map.Entry<DocumentKey, MutableDocument> docEntry : remoteDocuments.entrySet()) {
Mutation overlay = overlays.get(docEntry.getKey());
if (overlay != null) {
overlay.applyToLocalView(docEntry.getValue(), null, Timestamp.now());
Expand All @@ -355,14 +353,12 @@ private ImmutableSortedMap<DocumentKey, Document> getDocumentsMatchingCollection
/** Queries the remote documents and mutation queue, by doing a full collection scan. */
private ImmutableSortedMap<DocumentKey, Document>
getDocumentsMatchingCollectionQueryFromMutationQueue(Query query, IndexOffset offset) {
ImmutableSortedMap<DocumentKey, MutableDocument> remoteDocuments =
remoteDocumentCache.getAllDocumentsMatchingQuery(query, offset);
Map<DocumentKey, MutableDocument> remoteDocuments =
remoteDocumentCache.getAll(query.getPath(), offset);

// TODO(indexing): We should plumb sinceReadTime through to the mutation queue
List<MutationBatch> matchingBatches = mutationQueue.getAllMutationBatchesAffectingQuery(query);

remoteDocuments = addMissingBaseDocuments(matchingBatches, remoteDocuments);

for (MutationBatch batch : matchingBatches) {
for (Mutation mutation : batch.getMutations()) {
// Only process documents belonging to the collection.
Expand All @@ -375,18 +371,18 @@ private ImmutableSortedMap<DocumentKey, Document> getDocumentsMatchingCollection
if (document == null) {
// Create invalid document to apply mutations on top of
document = MutableDocument.newInvalidDocument(key);
remoteDocuments = remoteDocuments.insert(key, document);
remoteDocuments.put(key, document);
}
mutation.applyToLocalView(
document, FieldMask.fromSet(new HashSet<>()), batch.getLocalWriteTime());
if (!document.isFoundDocument()) {
remoteDocuments = remoteDocuments.remove(key);
remoteDocuments.remove(key);
}
}
}

ImmutableSortedMap<DocumentKey, Document> results = emptyDocumentMap();
for (Map.Entry<DocumentKey, MutableDocument> docEntry : remoteDocuments) {
for (Map.Entry<DocumentKey, MutableDocument> docEntry : remoteDocuments.entrySet()) {
// Finally, insert the documents that still match the query
if (query.matches(docEntry.getValue())) {
results = results.insert(docEntry.getKey(), docEntry.getValue());
Expand All @@ -395,35 +391,4 @@ private ImmutableSortedMap<DocumentKey, Document> getDocumentsMatchingCollection

return results;
}

/**
* It is possible that a {@code PatchMutation} can make a document match a query, even if the
* version in the {@code RemoteDocumentCache} is not a match yet (waiting for server to ack). To
* handle this, we find all document keys affected by the {@code PatchMutation}s that are not in
* {@code existingDocs} yet, and back fill them via {@code remoteDocumentCache.getAll}, otherwise
* those {@code PatchMutation}s will be ignored because no base document can be found, and lead to
* missing results for the query.
*/
private ImmutableSortedMap<DocumentKey, MutableDocument> addMissingBaseDocuments(
List<MutationBatch> matchingBatches,
ImmutableSortedMap<DocumentKey, MutableDocument> existingDocs) {
HashSet<DocumentKey> missingDocKeys = new HashSet<>();
for (MutationBatch batch : matchingBatches) {
for (Mutation mutation : batch.getMutations()) {
if (mutation instanceof PatchMutation && !existingDocs.containsKey(mutation.getKey())) {
missingDocKeys.add(mutation.getKey());
}
}
}

ImmutableSortedMap<DocumentKey, MutableDocument> mergedDocs = existingDocs;
Map<DocumentKey, MutableDocument> missingDocs = remoteDocumentCache.getAll(missingDocKeys);
for (Map.Entry<DocumentKey, MutableDocument> entry : missingDocs.entrySet()) {
if (entry.getValue().isFoundDocument()) {
mergedDocs = mergedDocs.insert(entry.getKey(), entry.getValue());
}
}

return mergedDocs;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import static com.google.firebase.firestore.util.Assert.hardAssert;

import androidx.annotation.Nullable;
import com.google.firebase.database.collection.ImmutableSortedMap;
import com.google.firebase.firestore.core.Target;
import com.google.firebase.firestore.model.Document;
import com.google.firebase.firestore.model.DocumentKey;
Expand Down Expand Up @@ -98,7 +99,7 @@ public Collection<FieldIndex> getFieldIndexes() {
}

@Override
public void updateIndexEntries(Collection<Document> documents) {
public void updateIndexEntries(ImmutableSortedMap<DocumentKey, Document> documents) {
// Field indices are not supported with memory persistence.
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ public MutationBatch addMutationBatch(
batchesByDocumentKey =
batchesByDocumentKey.insert(new DocumentReference(mutation.getKey(), batchId));

indexManager.addToCollectionParentIndex(mutation.getKey().getPath().popLast());
indexManager.addToCollectionParentIndex(mutation.getKey().getCollectionPath());
}

return batch;
Expand Down
Loading