Skip to content

Commit c7eb7da

Browse files
CoderickLamardplewis
authored andcommitted
Fix for count being very slow on large Parse Classes' collections (Postgres) (#5330)
* Changed count to be approximate. Should help with postgres slowness * refactored last commit to only fall back to estimate if no complex query * handlign variables correctly * Trying again because it was casting to lowercase table names which doesnt work for us/ * syntax error * Adding quotations to pg query * hopefully final pg fix * Postgres will now use an approximate count unless there is a more complex query specified * handling edge case * Fix for count being very slow on large Parse Classes' collections in Postgres. Replicating fix for Mongo in issue 5264 * Fixed silly spelling error resulting from copying over notes * Lint fixes * limiting results to 1 on approximation * suppress test that we can no longer run for postgres * removed tests from Postgres that no longer apply * made changes requested by dplewis * fixed count errors * updated package.json * removed test exclude for pg * removed object types from method * test disabled for postgres * returned type * add estimate count test * fix mongo test
1 parent e396612 commit c7eb7da

File tree

7 files changed

+181
-57
lines changed

7 files changed

+181
-57
lines changed

package-lock.json

Lines changed: 61 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

spec/AudienceRouter.spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ describe('AudiencesRouter', () => {
145145
});
146146
});
147147

148-
it('query installations with count = 1', done => {
148+
it_exclude_dbs(['postgres'])('query installations with count = 1', done => {
149149
const config = Config.get('test');
150150
const androidAudienceRequest = {
151151
name: 'Android Users',
@@ -189,7 +189,7 @@ describe('AudiencesRouter', () => {
189189
});
190190
});
191191

192-
it('query installations with limit = 0 and count = 1', done => {
192+
it_exclude_dbs(['postgres'])('query installations with limit = 0 and count = 1', done => {
193193
const config = Config.get('test');
194194
const androidAudienceRequest = {
195195
name: 'Android Users',

spec/InstallationsRouter.spec.js

Lines changed: 79 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ describe('InstallationsRouter', () => {
160160
});
161161
});
162162

163-
it('query installations with count = 1', done => {
163+
it_exclude_dbs(['postgres'])('query installations with count = 1', done => {
164164
const config = Config.get('test');
165165
const androidDeviceRequest = {
166166
installationId: '12345678-abcd-abcd-abcd-123456789abc',
@@ -209,7 +209,7 @@ describe('InstallationsRouter', () => {
209209
});
210210
});
211211

212-
it('query installations with limit = 0 and count = 1', done => {
212+
it_only_db('postgres')('query installations with count = 1', async () => {
213213
const config = Config.get('test');
214214
const androidDeviceRequest = {
215215
installationId: '12345678-abcd-abcd-abcd-123456789abc',
@@ -224,40 +224,90 @@ describe('InstallationsRouter', () => {
224224
auth: auth.master(config),
225225
body: {},
226226
query: {
227-
limit: 0,
228227
count: 1,
229228
},
230229
info: {},
231230
};
232231

233232
const router = new InstallationsRouter();
234-
rest
235-
.create(
236-
config,
237-
auth.nobody(config),
238-
'_Installation',
239-
androidDeviceRequest
240-
)
241-
.then(() => {
242-
return rest.create(
233+
await rest.create(
234+
config,
235+
auth.nobody(config),
236+
'_Installation',
237+
androidDeviceRequest
238+
);
239+
await rest.create(
240+
config,
241+
auth.nobody(config),
242+
'_Installation',
243+
iosDeviceRequest
244+
);
245+
let res = await router.handleFind(request);
246+
let response = res.response;
247+
expect(response.results.length).toEqual(2);
248+
expect(response.count).toEqual(0); // estimate count is zero
249+
250+
const pgAdapter = config.database.adapter;
251+
await pgAdapter.updateEstimatedCount('_Installation');
252+
253+
res = await router.handleFind(request);
254+
response = res.response;
255+
expect(response.results.length).toEqual(2);
256+
expect(response.count).toEqual(2);
257+
});
258+
259+
it_exclude_dbs(['postgres'])(
260+
'query installations with limit = 0 and count = 1',
261+
done => {
262+
const config = Config.get('test');
263+
const androidDeviceRequest = {
264+
installationId: '12345678-abcd-abcd-abcd-123456789abc',
265+
deviceType: 'android',
266+
};
267+
const iosDeviceRequest = {
268+
installationId: '12345678-abcd-abcd-abcd-123456789abd',
269+
deviceType: 'ios',
270+
};
271+
const request = {
272+
config: config,
273+
auth: auth.master(config),
274+
body: {},
275+
query: {
276+
limit: 0,
277+
count: 1,
278+
},
279+
info: {},
280+
};
281+
282+
const router = new InstallationsRouter();
283+
rest
284+
.create(
243285
config,
244286
auth.nobody(config),
245287
'_Installation',
246-
iosDeviceRequest
247-
);
248-
})
249-
.then(() => {
250-
return router.handleFind(request);
251-
})
252-
.then(res => {
253-
const response = res.response;
254-
expect(response.results.length).toEqual(0);
255-
expect(response.count).toEqual(2);
256-
done();
257-
})
258-
.catch(err => {
259-
fail(JSON.stringify(err));
260-
done();
261-
});
262-
});
288+
androidDeviceRequest
289+
)
290+
.then(() => {
291+
return rest.create(
292+
config,
293+
auth.nobody(config),
294+
'_Installation',
295+
iosDeviceRequest
296+
);
297+
})
298+
.then(() => {
299+
return router.handleFind(request);
300+
})
301+
.then(res => {
302+
const response = res.response;
303+
expect(response.results.length).toEqual(0);
304+
expect(response.count).toEqual(2);
305+
done();
306+
})
307+
.catch(err => {
308+
fail(JSON.stringify(err));
309+
done();
310+
});
311+
}
312+
);
263313
});

src/Adapters/Storage/Mongo/MongoStorageAdapter.js

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,7 @@ export class MongoStorageAdapter implements StorageAdapter {
152152
// encoded
153153
const encodedUri = formatUrl(parseUrl(this._uri));
154154

155-
this.connectionPromise = MongoClient.connect(
156-
encodedUri,
157-
this._mongoOptions
158-
)
155+
this.connectionPromise = MongoClient.connect(encodedUri, this._mongoOptions)
159156
.then(client => {
160157
// Starting mongoDB 3.0, the MongoClient.connect don't return a DB anymore but a client
161158
// Fortunately, we can get back the options and use them to select the proper DB.
@@ -385,8 +382,8 @@ export class MongoStorageAdapter implements StorageAdapter {
385382
deleteAllClasses(fast: boolean) {
386383
return storageAdapterAllCollections(this).then(collections =>
387384
Promise.all(
388-
collections.map(
389-
collection => (fast ? collection.deleteMany({}) : collection.drop())
385+
collections.map(collection =>
386+
fast ? collection.deleteMany({}) : collection.drop()
390387
)
391388
)
392389
);
@@ -952,6 +949,8 @@ export class MongoStorageAdapter implements StorageAdapter {
952949
readPreference = ReadPreference.NEAREST;
953950
break;
954951
case undefined:
952+
case null:
953+
case '':
955954
break;
956955
default:
957956
throw new Parse.Error(

src/Adapters/Storage/Postgres/PostgresStorageAdapter.js

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1962,17 +1962,37 @@ export class PostgresStorageAdapter implements StorageAdapter {
19621962
}
19631963

19641964
// Executes a count.
1965-
count(className: string, schema: SchemaType, query: QueryType) {
1966-
debug('count', className, query);
1965+
count(
1966+
className: string,
1967+
schema: SchemaType,
1968+
query: QueryType,
1969+
readPreference?: string,
1970+
estimate?: boolean = true
1971+
) {
1972+
debug('count', className, query, readPreference, estimate);
19671973
const values = [className];
19681974
const where = buildWhereClause({ schema, query, index: 2 });
19691975
values.push(...where.values);
19701976

19711977
const wherePattern =
19721978
where.pattern.length > 0 ? `WHERE ${where.pattern}` : '';
1973-
const qs = `SELECT count(*) FROM $1:name ${wherePattern}`;
1979+
let qs = '';
1980+
1981+
if (where.pattern.length > 0 || !estimate) {
1982+
qs = `SELECT count(*) FROM $1:name ${wherePattern}`;
1983+
} else {
1984+
qs =
1985+
'SELECT reltuples AS approximate_row_count FROM pg_class WHERE relname = $1';
1986+
}
1987+
19741988
return this._client
1975-
.one(qs, values, a => +a.count)
1989+
.one(qs, values, a => {
1990+
if (a.approximate_row_count != null) {
1991+
return +a.approximate_row_count;
1992+
} else {
1993+
return +a.count;
1994+
}
1995+
})
19761996
.catch(error => {
19771997
if (error.code !== PostgresRelationDoesNotExistError) {
19781998
throw error;
@@ -2327,6 +2347,11 @@ export class PostgresStorageAdapter implements StorageAdapter {
23272347
updateSchemaWithIndexes(): Promise<void> {
23282348
return Promise.resolve();
23292349
}
2350+
2351+
// Used for testing purposes
2352+
updateEstimatedCount(className: string) {
2353+
return this._client.none('ANALYZE $1:name', [className]);
2354+
}
23302355
}
23312356
23322357
function convertPolygonToSQL(polygon) {

src/Adapters/Storage/StorageAdapter.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ export interface StorageAdapter {
8686
className: string,
8787
schema: SchemaType,
8888
query: QueryType,
89-
readPreference: ?string
89+
readPreference?: string,
90+
estimate?: boolean
9091
): Promise<number>;
9192
distinct(
9293
className: string,

src/Controllers/DatabaseController.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1324,7 +1324,9 @@ class DatabaseController {
13241324
})
13251325
.then((schema: any) => {
13261326
return this.collectionExists(className)
1327-
.then(() => this.adapter.count(className, { fields: {} }))
1327+
.then(() =>
1328+
this.adapter.count(className, { fields: {} }, null, '', false)
1329+
)
13281330
.then(count => {
13291331
if (count > 0) {
13301332
throw new Parse.Error(

0 commit comments

Comments
 (0)