Merge pull request #15554 from strapi/fix/performance-issues-clean-order-mysql

Improve SQL queries when cleaning orders
This commit is contained in:
Pierre Noël 2023-01-25 15:53:15 +01:00 committed by GitHub
commit 85a9359756
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -198,38 +198,73 @@ const cleanOrderColumns = async ({ id, attribute, db, inverseRelIds, transaction
return; return;
} }
// Handle databases that don't support window function ROW_NUMBER (here it's MySQL 5)
if (!strapi.db.dialect.supportsWindowFunctions()) {
await cleanOrderColumnsForOldDatabases({ id, attribute, db, inverseRelIds, transaction: trx });
return;
}
const { joinTable } = attribute;
const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable;
const update = [];
const updateBinding = [];
const select = ['??'];
const selectBinding = ['id'];
const where = [];
const whereBinding = [];
if (hasOrderColumn(attribute) && id) {
update.push('?? = b.src_order');
updateBinding.push(orderColumnName);
select.push('ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ??) AS src_order');
selectBinding.push(joinColumn.name, orderColumnName);
where.push('?? = ?');
whereBinding.push(joinColumn.name, id);
}
if (hasInverseOrderColumn(attribute) && !isEmpty(inverseRelIds)) {
update.push('?? = b.inv_order');
updateBinding.push(inverseOrderColumnName);
select.push('ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ??) AS inv_order');
selectBinding.push(inverseJoinColumn.name, inverseOrderColumnName);
where.push(`?? IN (${inverseRelIds.map(() => '?').join(', ')})`);
whereBinding.push(inverseJoinColumn.name, ...inverseRelIds);
}
switch (strapi.db.dialect.client) { switch (strapi.db.dialect.client) {
case 'mysql': case 'mysql':
await cleanOrderColumnsForInnoDB({ id, attribute, db, inverseRelIds, transaction: trx }); // Here it's MariaDB and MySQL 8
await db
.getConnection()
.raw(
`UPDATE
?? as a,
(
SELECT ${select.join(', ')}
FROM ??
WHERE ${where.join(' OR ')}
) AS b
SET ${update.join(', ')}
WHERE b.id = a.id`,
[joinTable.name, ...selectBinding, joinTable.name, ...whereBinding, ...updateBinding]
)
.transacting(trx);
break; break;
/*
UPDATE
:joinTable: as a,
(
SELECT
id,
ROW_NUMBER() OVER ( PARTITION BY :joinColumn: ORDER BY :orderColumn:) AS src_order,
ROW_NUMBER() OVER ( PARTITION BY :inverseJoinColumn: ORDER BY :inverseOrderColumn:) AS inv_order
FROM :joinTable:
WHERE :joinColumn: = :id OR :inverseJoinColumn: IN (:inverseRelIds)
) AS b
SET :orderColumn: = b.src_order, :inverseOrderColumn: = b.inv_order
WHERE b.id = a.id;
*/
default: { default: {
const { joinTable } = attribute;
const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable;
const update = [];
const updateBinding = [];
const select = ['??'];
const selectBinding = ['id'];
const where = [];
const whereBinding = [];
if (hasOrderColumn(attribute) && id) {
update.push('?? = b.src_order');
updateBinding.push(orderColumnName);
select.push('ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ??) AS src_order');
selectBinding.push(joinColumn.name, orderColumnName);
where.push('?? = ?');
whereBinding.push(joinColumn.name, id);
}
if (hasInverseOrderColumn(attribute) && !isEmpty(inverseRelIds)) {
update.push('?? = b.inv_order');
updateBinding.push(inverseOrderColumnName);
select.push('ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ??) AS inv_order');
selectBinding.push(inverseJoinColumn.name, inverseOrderColumnName);
where.push(`?? IN (${inverseRelIds.map(() => '?').join(', ')})`);
whereBinding.push(inverseJoinColumn.name, ...inverseRelIds);
}
const joinTableName = addSchema(joinTable.name); const joinTableName = addSchema(joinTable.name);
// raw query as knex doesn't allow updating from a subquery // raw query as knex doesn't allow updating from a subquery
@ -249,17 +284,17 @@ const cleanOrderColumns = async ({ id, attribute, db, inverseRelIds, transaction
.transacting(trx); .transacting(trx);
/* /*
`UPDATE :joinTable: as a UPDATE :joinTable: as a
SET :orderColumn: = b.src_order, :inverseOrderColumn: = b.inv_order SET :orderColumn: = b.src_order, :inverseOrderColumn: = b.inv_order
FROM ( FROM (
SELECT SELECT
id, id,
ROW_NUMBER() OVER ( PARTITION BY :joinColumn: ORDER BY :orderColumn:) AS src_order, ROW_NUMBER() OVER ( PARTITION BY :joinColumn: ORDER BY :orderColumn:) AS src_order,
ROW_NUMBER() OVER ( PARTITION BY :inverseJoinColumn: ORDER BY :inverseOrderColumn:) AS inv_order ROW_NUMBER() OVER ( PARTITION BY :inverseJoinColumn: ORDER BY :inverseOrderColumn:) AS inv_order
FROM :joinTable: FROM :joinTable:
WHERE :joinColumn: = :id OR :inverseJoinColumn: IN (:inverseRelIds) WHERE :joinColumn: = :id OR :inverseJoinColumn: IN (:inverseRelIds)
) AS b ) AS b
WHERE b.id = a.id`, WHERE b.id = a.id;
*/ */
} }
} }
@ -267,9 +302,9 @@ const cleanOrderColumns = async ({ id, attribute, db, inverseRelIds, transaction
/* /*
* Ensure that orders are following a 1, 2, 3 sequence, without gap. * Ensure that orders are following a 1, 2, 3 sequence, without gap.
* The use of a temporary table instead of a window function makes the query compatible with MySQL 5 and prevents some deadlocks to happen in innoDB databases * The use of a session variable instead of a window function makes the query compatible with MySQL 5
*/ */
const cleanOrderColumnsForInnoDB = async ({ const cleanOrderColumnsForOldDatabases = async ({
id, id,
attribute, attribute,
db, db,
@ -279,96 +314,68 @@ const cleanOrderColumnsForInnoDB = async ({
const { joinTable } = attribute; const { joinTable } = attribute;
const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable; const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable;
const now = new Date().valueOf(); const randomSuffix = `${new Date().valueOf()}_${randomBytes(16).toString('hex')}`;
const randomHex = randomBytes(16).toString('hex');
if (hasOrderColumn(attribute) && id) { if (hasOrderColumn(attribute) && id) {
const tempOrderTableName = `orderTable_${now}_${randomHex}`; // raw query as knex doesn't allow updating from a subquery
try { // https://github.com/knex/knex/issues/2504
await db.connection const orderVar = `order_${randomSuffix}`;
.raw( await db.connection.raw(`SET @${orderVar} = 0;`).transacting(trx);
` await db.connection
CREATE TABLE :tempOrderTableName: .raw(
SELECT `UPDATE :joinTableName: as a, (
id, SELECT id, (@${orderVar}:=@${orderVar} + 1) AS src_order
( FROM :joinTableName:
SELECT count(*) WHERE :joinColumnName: = :id
FROM :joinTableName: b ORDER BY :orderColumnName:
WHERE a.:orderColumnName: >= b.:orderColumnName: AND a.:joinColumnName: = b.:joinColumnName: AND a.:joinColumnName: = :id ) AS b
) AS src_order SET :orderColumnName: = b.src_order
FROM :joinTableName: a WHERE a.id = b.id
WHERE a.:joinColumnName: = :id AND a.:joinColumnName: = :id`,
`, {
{ joinTableName: joinTable.name,
tempOrderTableName, orderColumnName,
joinTableName: joinTable.name, joinColumnName: joinColumn.name,
orderColumnName, id,
joinColumnName: joinColumn.name, }
id, )
} .transacting(trx);
)
.transacting(trx);
// raw query as knex doesn't allow updating from a subquery
// https://github.com/knex/knex/issues/2504
await db.connection
.raw(
`UPDATE ?? as a, (SELECT * FROM ??) AS b
SET ?? = b.src_order
WHERE a.id = b.id`,
[joinTable.name, tempOrderTableName, orderColumnName]
)
.transacting(trx);
} finally {
await db.connection.raw(`DROP TABLE IF EXISTS ??`, [tempOrderTableName]).transacting(trx);
}
} }
if (hasInverseOrderColumn(attribute) && !isEmpty(inverseRelIds)) { if (hasInverseOrderColumn(attribute) && !isEmpty(inverseRelIds)) {
const tempInvOrderTableName = `invOrderTable_${now}_${randomHex}`; const orderVar = `order_${randomSuffix}`;
try { const columnVar = `col_${randomSuffix}`;
await db.connection await db.connection.raw(`SET @${orderVar} = 0;`).transacting(trx);
.raw( await db.connection
` .raw(
CREATE TABLE ?? `UPDATE ?? as a, (
SELECT SELECT
id, id,
( @${orderVar}:=CASE WHEN @${columnVar} = ?? THEN @${orderVar} + 1 ELSE 1 END AS inv_order,
SELECT count(*) @${columnVar}:=?? ??
FROM ?? b FROM ?? a
WHERE a.?? >= b.?? AND a.?? = b.?? AND a.?? IN (${inverseRelIds WHERE ?? IN(${inverseRelIds.map(() => '?').join(', ')})
.map(() => '?') ORDER BY ??, ??
.join(', ')}) ) AS b
) AS inv_order SET ?? = b.inv_order
FROM ?? a WHERE a.id = b.id
WHERE a.?? IN (${inverseRelIds.map(() => '?').join(', ')}) AND a.?? IN(${inverseRelIds.map(() => '?').join(', ')})`,
`, [
[ joinTable.name,
tempInvOrderTableName, inverseJoinColumn.name,
joinTable.name, inverseJoinColumn.name,
inverseOrderColumnName, inverseJoinColumn.name,
inverseOrderColumnName, joinTable.name,
inverseJoinColumn.name, inverseJoinColumn.name,
inverseJoinColumn.name, ...inverseRelIds,
inverseJoinColumn.name, inverseJoinColumn.name,
...inverseRelIds, joinColumn.name,
joinTable.name, inverseOrderColumnName,
inverseJoinColumn.name, inverseJoinColumn.name,
...inverseRelIds, ...inverseRelIds,
] ]
) )
.transacting(trx); .transacting(trx);
await db.connection
.raw(
`UPDATE ?? as a, (SELECT * FROM ??) AS b
SET ?? = b.inv_order
WHERE a.id = b.id`,
[joinTable.name, tempInvOrderTableName, inverseOrderColumnName]
)
.transacting(trx);
} finally {
await db.connection.raw(`DROP TABLE IF EXISTS ??`, [tempInvOrderTableName]).transacting(trx);
}
} }
}; };