Ce qui se passe
Avec Django, on choisit entre QuerySet.update() — une instruction SQL, pas de
signaux — et une boucle de save() — un ordre par ligne, signaux émis. NocoBase
n'offre pas ce choix : toute mise à jour filtrée passe par la seconde forme. Un
développeur a lu le code au tag v2.2.7, capturé le SQL avec log_statement=all
sur PostgreSQL 16, et mesuré ce que ça coûte.
Le mécanisme
La méthode update de packages/core/database/src/repository.ts cherche les
lignes avec le filtre, n'en retient que les clés primaires, puis écrit sur ces
clés. La condition ne franchit jamais la frontière :
packages/core/database/src/repository.ts (v2.2.7)TypeScriptconst queryOptions = this.buildQueryOptions({ ...options, fields: [primaryKeyField] });
const rows = await this.find({ ...queryOptions, transaction });
const [result] = await Model.update(values, {
where: { [primaryKeyField]: rows.map((row) => row.get(primaryKeyField)) },
});Le journal PostgreSQL le confirme : WHERE status = 'pending' apparaît une seule
fois, dans le SELECT.
SELECT "id","status","note" FROM "public"."orders" AS "orders"
WHERE "orders"."status" = 'pending' ORDER BY "orders"."id" ASC NULLS LAST;
UPDATE "public"."orders" SET "note"=$1 WHERE "id" = $2; -- ligne 1
UPDATE "public"."orders" SET "note"=$1 WHERE "id" = $2; -- ligne 2
COMMIT;Entre le SELECT et chaque UPDATE, rien ne revérifie rien — et la transaction
n'y change rien, puisqu'il n'y a aucune condition à revalider.
La mesure
10 001 lignes en status = 'pending', réécriture de note sur toutes, et à la
première seconde on bascule une seule ligne — la dernière à être traitée — en
cancelled.
| Résultat | Valeur |
|---|---|
| Durée de la mise à jour en masse | 6,2 à 6,4 s |
| Ligne sortie de la condition, écrite quand même | 5 fois sur 5 |
Avec un seul UPDATE ... WHERE |
10 000 lignes sur 10 001, la bonne exclue |
| Coût des 10 002 instructions | 9 fois plus lent qu'une seule |
La fenêtre s'élargit avec le volume : sur 2 002 lignes, l'opération se termine en 1,5 s et l'interruption programmée à 2 s arrive trop tard. Même mécanisme, intervalle plus court — voilà pourquoi une petite table ne montre jamais rien.
Ce que ça change
Ce n'est pas un arbitrage de performance : la lenteur achète les événements de
collection, que le nœud SQL d'un workflow ne déclenche pas. La branche
individualHooks: false émet bien un seul UPDATE, avec ce commentaire dans la
source — « would be more efficient up to 100000 ~ 1000000 rows » — mais elle
reste indexée sur les clés primaires, et l'API REST ne peut pas l'atteindre.
En attendant, deux habitudes :
- Ne pas traiter « seules les lignes qui remplissent le filtre » comme une garantie. Un filtre désigne une cible ; il ne promet pas qu'elle y soit encore au moment de l'écriture. Validations, allocations, clôtures de période : partout où toucher une ligne devenue hors critère est un vrai problème, il faut autre chose.
- Quand l'atomicité prime sur les événements, passer par un nœud SQL dans un workflow.
À retenir
Le signalement est arrivé sur le forum officiel le 4 septembre (t/14097),
avec une réponse de l'équipe — « nous enquêtons » — et rien depuis. Le fichier
repository.ts a été comparé entre v2.2.7, v2.2.8, v2.2.12, v2.4.0-alpha.4 et
v2.4.0-alpha.5 : les cinq sont identiques à l'octet, 1 009 lignes, même
empreinte MD5. Au 12 septembre, 2.2.12 est la version courante et
2.4.0-alpha.5 l'alpha.
La mesure ne porte que sur PostgreSQL, mais la condition étant abandonnée dans le
code applicatif, la base ne devrait rien y changer. Et la question se pose telle
quelle à n'importe quel ORM : est-ce que ma condition arrive jusqu'au WHERE de
l'UPDATE ?