Merge getBySku into single SQL query with FOR JSON sub-queries

Replaces 6 round-trips (1 sequential resolve + 5 parallel detail)
with one query. Auth resolution, article detail, categories, photos,
eans, and prices are all fetched in a single round-trip.
This commit is contained in:
seb
2026-08-03 08:39:17 +02:00
parent b3ef24f34a
commit 14d8a4c978
2 changed files with 210 additions and 13 deletions

View File

@@ -159,3 +159,152 @@ SELECT id
FROM brand FROM brand
WHERE id = @brandId AND company_id = @companyId WHERE id = @brandId AND company_id = @companyId
`; `;
/**
* Single-query replacement for resolveArticleBySku + queryBySku.
* Resolves SKU → article + company + auth, then loads full article detail
* including categories, photos, eans, and prices via FOR JSON sub-queries.
* Returns one row with articleJson, or empty if not found/unauthorized.
*/
export const GET_ARTICLE_BY_SKU_FULL = `
;WITH article_cat AS (
SELECT TOP 1 a.id AS articleId, ca.category_id AS categoryId
FROM article a
INNER JOIN categoryarticle ca ON ca.article_id = a.id
WHERE a.sku = @sku
),
ancestors AS (
SELECT c.id, c.parent_id
FROM category c
INNER JOIN article_cat ac ON c.id = ac.categoryId
UNION ALL
SELECT c.id, c.parent_id
FROM category c
INNER JOIN ancestors a ON c.id = a.parent_id
),
root AS (
SELECT TOP 1 id AS rootCategoryId
FROM ancestors
WHERE parent_id IS NULL
),
auth AS (
SELECT
ac.articleId,
ac.categoryId,
r.rootCategoryId,
co.id AS companyId,
co.default_language_id AS languageId,
co.poscustomergroup_id AS posCustomerGroupId,
cu.id AS companyuserId
FROM article_cat ac
LEFT JOIN root r ON 1 = 1
LEFT JOIN company co ON co.root_category_id = r.rootCategoryId
LEFT JOIN companyuser cu
ON cu.company_id = co.id AND cu.google_auth_id = @googleAuthId
)
SELECT
auth.articleId,
auth.categoryId,
auth.rootCategoryId,
auth.companyId,
auth.languageId,
auth.posCustomerGroupId,
auth.companyuserId,
(
SELECT
a.id,
a.sku,
at.name,
at.slug,
a.unitamount,
a.weight,
u.si AS unitSi,
u.basesi AS unitBasesi,
u.basesifactor AS unitBasesifactor,
ut.symbol AS unitSymbol,
ut.name AS unitName,
b.name AS brandName,
bp.thumbw AS brandThumbW,
bp.thumbh AS brandThumbH,
bp.thumbhash AS brandThumbHash,
(
SELECT COUNT(*)
FROM optionslistarticle ola
WHERE ola.optionslist_id = a.optionslist_id
) AS optionsCount,
(
SELECT COUNT(*)
FROM articlelistarticle ala
WHERE ala.articlelist_id = a.articlelist_id
) AS articleListCount,
(
SELECT COUNT(*)
FROM article child
WHERE child.fatherarticle_id = a.id
) AS childCount,
a.fatherarticle_id AS fatherArticleId,
fa.sku AS fatherSku,
(
SELECT ca2.category_id
FROM categoryarticle ca2
WHERE ca2.article_id = a.id
ORDER BY ca2.category_id
FOR JSON PATH
) AS categoryIdsJson,
(
SELECT
ap.sort,
p.hash AS fullHash,
p.w AS fullW,
p.h AS fullH,
p.size AS fullSize,
p.thumbw AS thumbW,
p.thumbh AS thumbH,
p.thumbhash AS thumbHash
FROM articlepic ap
INNER JOIN pic p ON p.id = ap.pic_id
WHERE ap.article_id = a.id
ORDER BY ap.sort ASC, ap.id ASC
FOR JSON PATH
) AS photosJson,
(
SELECT ae.ean
FROM articleean ae
WHERE ae.article_id = a.id
ORDER BY ae.ean
FOR JSON PATH
) AS eansJson,
(
SELECT
pr.minmultiplier,
pr.net,
t.rate AS taxRate,
pr.customergroup_id AS customergroupId
FROM price pr
INNER JOIN tax t ON t.id = pr.tax_id
WHERE pr.article_id = a.id
ORDER BY pr.customergroup_id, pr.minmultiplier
FOR JSON PATH
) AS pricesJson
FROM article a
LEFT JOIN articletext at
ON at.article_id = a.id AND at.language_id = auth.languageId
LEFT JOIN unit u ON u.id = a.unit_id
LEFT JOIN unittext ut
ON ut.unit_id = u.id AND ut.language_id = auth.languageId
LEFT JOIN brand b ON b.id = a.brand_id
OUTER APPLY (
SELECT TOP 1 bp2.pic_id
FROM brandpic bp2
WHERE bp2.brand_id = b.id
ORDER BY bp2.sort ASC, bp2.id ASC
) first_brand_pic
LEFT JOIN pic bp ON bp.id = first_brand_pic.pic_id
LEFT JOIN article fa ON fa.id = a.fatherarticle_id
WHERE a.id = auth.articleId
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
) AS articleJson
FROM auth
`;

View File

@@ -1,22 +1,70 @@
import resolveArticleBySku from './resolveArticleBySku.js'; import { GET_ARTICLE_BY_SKU_FULL } from '../../sql/article.js';
import queryBySku from './queryBySku.js';
function parseJsonArray(raw) {
if (!raw) return [];
const arr = typeof raw === 'string' ? JSON.parse(raw) : raw;
return Array.isArray(arr) ? arr : [];
}
export default function getBySku(pool) { export default function getBySku(pool) {
const resolve = resolveArticleBySku(pool);
const load = queryBySku(pool);
return async (googleAuthId, sku) => { return async (googleAuthId, sku) => {
let t0 = performance.now(); const t0 = performance.now();
const { articleId, languageId } = await resolve(googleAuthId, sku); const result = await pool
console.log(`[getBySku] resolveArticleBySku ${(performance.now() - t0).toFixed(1)}ms`); .request()
.input('sku', sku)
.input('googleAuthId', googleAuthId)
.query(GET_ARTICLE_BY_SKU_FULL);
console.log(`[getBySku] single query ${(performance.now() - t0).toFixed(1)}ms`);
t0 = performance.now(); const row = result.recordset[0];
const article = await load(articleId, languageId); if (!row) {
console.log(`[getBySku] queryBySku (parallel) ${(performance.now() - t0).toFixed(1)}ms`);
if (!article) {
throw new Error('Article not found'); throw new Error('Article not found');
} }
if (!row.articleId || !row.categoryId) {
throw new Error('Article not found');
}
if (!row.companyId || !row.rootCategoryId || !row.languageId) {
throw new Error('Parent not found');
}
if (!row.companyuserId) {
throw new Error('Not authorized');
}
return article; const a = typeof row.articleJson === 'string' ? JSON.parse(row.articleJson) : row.articleJson;
return {
article: {
id: a.id,
sku: a.sku,
name: a.name,
slug: a.slug,
unitamount: a.unitamount,
weight: a.weight,
unit: {
si: a.unitSi,
basesi: a.unitBasesi,
basesifactor: a.unitBasesifactor,
symbol: a.unitSymbol ?? null,
name: a.unitName ?? null,
},
brand: a.brandName
? {
name: a.brandName,
thumbW: a.brandThumbW,
thumbH: a.brandThumbH,
thumbHash: a.brandThumbHash,
}
: null,
categoryIds: parseJsonArray(a.categoryIdsJson).map((r) => r.category_id),
photos: parseJsonArray(a.photosJson),
eans: parseJsonArray(a.eansJson).map((r) => r.ean),
prices: parseJsonArray(a.pricesJson),
optionsCount: a.optionsCount,
articleListCount: a.articleListCount,
childCount: a.childCount,
fatherArticleId: a.fatherArticleId,
fatherSku: a.fatherSku,
},
};
}; };
} }