== Индекси Индексите ги забрзуваат операциите за пребарување и филтрирање, што значително го намалува времето на извршување на SQL барања. Индексирањето има смисла ако ќе ја подобри брзината на наредбите '''SELECT, JOIN, WHERE, ORDER BY''' и '''GROUP BY'''. Направивме измени во db.js така што '''{{{Client}}}''' го заменивме за '''{{{Pool}}}''' \\ бидејки client секогаш отвара нова конекција а со pool користи збир конекции за подобра оптимизација. \\ Кога зборуваме за поставување на барања до база, на старата верзија тоа беше веројатно многу по лимитирано, \\ а сега имаме овозможено повеќе истовремени конекции т.е. {{{max: 20}}}. \\ Во нашиот код рачно се отвора и затвора конекција, додека новиот код користи '''{{{getClient()}}}''' за подобра контрола над конекциите. Освен тоа при пад на конекција потребно е restart а сега имаме момент каде '''{{{pool.on("error")}}}''' ја фаќа грешката. Да не заборавиме и на Timeout моментот, сега имаме поставено {{{idleTimeoutMillis}}} и {{{connectionTimeoutMillis}}} за подобро управување.Употребуваме и {{{try-catch}}} блок за да регистрира евентуални грешки при извршување на {{{CREATE INDEX}}}.\\ \\ {{{#!sql module.exports = {   query: (text, params) => pool.query(text, params),   getClient: async () => {     const client = await pool.connect();     return client;   }, }; }}} == Што е важно за индекси? -> Примарни клучеви да се индексирани автоматски \\ -> Најчесто пребарувани колони (особено во WHERE, JOIN, ORDER BY или GROUP BY) треба да имаат индекси \\ -> Надворешните клучеви треба да имаат индекси за побрзи JOIN операции \\ -> Колони што се често филтрирани или сортирани треба да имаат индекси \\ \\ '''Индекси за подобрување на {{{JOIN}}} перформанси''' {{{#!sql CREATE INDEX idx_customers_email ON customers(email); CREATE INDEX idx_orders_customer_id ON orders(id_customer); CREATE INDEX idx_delivery_order_id ON delivery(id_order); CREATE INDEX idx_products_category_id ON products(id_category); CREATE INDEX idx_stock_product_id ON stock(id_product); CREATE INDEX idx_contains_order_id ON contains(id_order); CREATE INDEX idx_contains_stock_id ON contains(id_stock); }}} \\ '''Индекси за оптимизирање на пребарувања со {{{WHERE}}}''' {{{#!sql CREATE INDEX idx_orders_status ON orders(status); CREATE INDEX idx_stock_price ON stock(price); CREATE INDEX idx_products_name ON products(product_name); }}} \\ ''' Тестирање на {{{CREATE INDEX}}} наредби ''' \\ [[Image(testiranjeCREATEINDEX.png, height=300px)]] \\ \\ '''После направените измени, кодот додава индекси автоматски ако не постојат со опцијата\\ {{{CREATE INDEX IF NOT EXISTS}}} и ги подобривме перформансите на JOIN, WHERE, ORDER BY со индексирање притоа сега се избегнуваат грешки ако индексите веќе постојат. \\''' \\ \\ == Додатни чекори за оптимизација на индекси со EXPLAIN ANALYZE Сега ке направиме мало тестирање со опцијата во прилог, \\ доколку Dbeaver користи Seq Scan (Sequence Scan), значи дека треба подобар индекс. {{{#!sql EXPLAIN ANALYZE SELECT * FROM orders WHERE id_customer = 5; }}} [[Image(explain.png, height=300px)]] \\ Бидејки на ова барање добивме одговор со {{{Seq Scan}}} наместо со {{{Index Scan}}},\\ ова во наш случај не би требало да е проблем бидејки базата нема премногу податоци и табелите се мали.\\ Алтернативно пробано е и со опција {{{ANALYZE my_table;}}}, па од прилог може да се провери во база дека индексот постои, но веројатно е поради мала табела. \\ \\ == Update '''Сценарио: Нарачка на продукт (избор на продукт-успешно купување)'''\\ \\ '''Кои табели и операции се потребни за оваа сценарио:'''\\ '''products''' — читање информации за продуктот\\ '''stock + sizes''' — читање залиха по големина\\ '''customers''' — проверка и/или внесување корисник\\ '''orders''' — внесување нова нарачка\\ '''contains''' — внесување детали за нарачаните продукти\\ '''payments''' — внесување плаќање\\ '''roles''' — читање на улоги (при проверка на корисничка улога)\\ \\ '''Клучни операции во backend:'''\\ {{{SELECT}}} со {{{JOIN}}} (пример: products со stock и sizes, customers со roles)\\ {{{INSERT}}} во повеќе табели (orders, contains, payments, customers, addresses)\\ {{{UPDATE}}} на stock(за намалување залиха)\\ {{{SELECT}}} со {{{FOR UPDATE}}} (за заклучување редови)\\ \\ = '''1. Индекси на {{{stock}}} табела''' Во документот app.js имам поставено на почеток два индекси \\ Кои се користат при пребарување залиха за одреден производ и големина (како во /order endpoint) како и во\\ SQL прашањето со {{{FOR UPDATE}}} за заклучување редови пред ажурирање.\\ Овие индекси го забрзуваат пребарувањето на редови во '''stock''' по {{{id_product}}} и {{{id_size}}}.\\ {{{#!sql CREATE INDEX IF NOT EXISTS idx_stock_product ON stock(id_product); CREATE INDEX IF NOT EXISTS idx_stock_size ON stock(id_size); }}} Тие се користат во API '''/order''' при проверка и заклучување на залиха \\ {{{#!sql SELECT s.id_stock, s.quantity FROM stock s JOIN sizes sz ON s.id_size = sz.id WHERE s.id_product = $1 AND sz.size_label = $2 ORDER BY s.created_at ASC FOR UPDATE }}} Во овој пример индексот на {{{id_product}}} помага брзо да се филтрира по производ \\ а другиот индекс на {{{id_size}}} помага за филтрирање според големина.\\ Исто така користам {{{WHERE}}} услов на двете колони {{{id_product}}} и {{{id_size}}},што значи дека '''композитен индекс'''\\ би била подобра опција отколку два посебни индекси бидејки ќе ги забрза и двата филтри во еден чекор.\\ Затоа го додавам овој дел во редот на индекси во '''db.pool.query''' во '''app.js''' : {{{#!sql CREATE INDEX idx_stock_product_size ON stock(id_product, id_size); }}} \\ \\ = '''2. Индекси на {{{products}}} табела'''\\ Во документот app.js имам поставено основен индекс на примарниот клуч {{{id_product}}}.\\ {{{#!sql CREATE INDEX IF NOT EXISTS idx_products_category ON products(id_category); }}} \\ Конкретно, кај '''stock''' и '''sizes''' употребив {{{JOIN}}} и {{{WHERE}}} услов за {{{id_product}}} : {{{#!sql // Детали за еден продукт со залиха > 0 app.get("/products/:id", async (req, res) => { const productId = parseInt(req.params.id); try { const productResult = await db.pool.query( `SELECT id_product, product_name, description, price, color, image_url FROM products WHERE id_product = $1`, [productId] ); if (productResult.rows.length === 0) { return res.status(404).json({ error: "Производот не е пронајден." }); } const stockResult = await db.pool.query( `SELECT sz.size_label AS size, s.quantity, p.price FROM stock s JOIN sizes sz ON s.id_size = sz.id JOIN products p ON s.id_product = p.id_product WHERE s.id_product = $1 AND s.quantity > 0 ORDER BY CASE sz.size_label WHEN 'S' THEN 1 WHEN 'M' THEN 2 WHEN 'L' THEN 3 WHEN 'XL' THEN 4 ELSE 5 END`, [productId] ); }}} \\ За сега овој индекс за оваа табела е доволен, доколку понатаму го надоградуваме системот ке биде потребно \\ индексирање за пребарување по име на продукт или филтрирање по други атрибути како боја, цена... \\ = '''3. Индекси на {{{customers}}} табелата'''\\ При најава на корисник е потребно внес на {{{email}}} и {{{id_role}}}, па затоа ке поставиме индекси.\\ Индекс на {{{email}}} ке го забрза пребарувањето на корисници при најава и регистрација.\\ Индекс на {{{id_role}}} го забрзува филтрирањето по улога, односно дали е корисник или админ.\\ Бидејки веќе користиме {{{#!sql SELECT * FROM customers WHERE email = $1 UPDATE customers SET id_role = $1 WHERE id_customer = $2 }}} \\ Додаваме овој дел во редот на индекси во '''db.pool.query''' во '''app.js''' : {{{#!sql CREATE UNIQUE INDEX idx_customers_email ON customers(email); CREATE INDEX idx_customers_role ON customers(id_role); }}} \\ = '''4. Индекси на {{{orders}}} табела'''\\ Поради филтрирањата на admin панелот кои ги правиме и делот {{{/admin/reports}}} \\ кои се филтрираат по order_date (неделно, месечно, годишно) преку следниот ред : {{{#!sql // --- Админ API: Извештаи за нарачки --- app.get('/admin/reports', authenticateToken, authorizeAdmin, async (req, res) => { const filter = req.query.filter; // weekly, monthly, yearly let query; if (filter === 'weekly') { query = ` SELECT COUNT(*) AS total_orders, COALESCE(SUM(total_price), 0) AS total_revenue FROM orders WHERE order_date >= NOW() - INTERVAL '7 days' `; } else if (filter === 'monthly') { query = ` SELECT COUNT(*) AS total_orders, COALESCE(SUM(total_price), 0) AS total_revenue FROM orders WHERE order_date >= NOW() - INTERVAL '1 month' `; } else if (filter === 'yearly') { query = ` SELECT COUNT(*) AS total_orders, COALESCE(SUM(total_price), 0) AS total_revenue FROM orders WHERE order_date >= NOW() - INTERVAL '1 year' `; } else { return res.status(400).json({ error: 'Невалиден филтер' }); } try { const result = await db.pool.query(query); res.json(result.rows[0]); } catch (err) { console.error(err); res.status(500).json({ error: 'Грешка при вчитување на извештај' }); } }); }}} \\ Го додаваме следниов индекс, кој го забрзува времето за филтирањето и посебно ако направиме посложени филтрирања :\\ {{{#!sql CREATE INDEX idx_orders_order_date ON orders(order_date); }}} \\ = '''5. Индекси на {{{contains}}} табела'''\\ Овде често користиме {{{id_order}}} и {{{id_stock}}} за поврзување со '''orders''' и '''stock''' Затоа ке креираме индекс : {{{#!sql CREATE INDEX idx_contains_order ON contains(id_order); CREATE INDEX idx_contains_stock ON contains(id_stock); }}} \\