Il paradigma MapReduce è alla base di framework di calcolo parallelo come Apache Hadoop per elaborare enormi quantità di dati.
Si fonda su un tipo di programmazione funzionale, dove cioè si possono passare funzioni come argomento di altre funzioni, che a loro volta restituiscono funzioni, e così via.
In questo modo si possono scrivere algoritmi molto compatti ed eleganti, anche se un po' criptici.
In Javascript possiamo combinare la potente programmazione funzionale con la semplicità nella gestione degli array in modo molto utile e divertente.
Per gli esempi utilizzati mi sono ispirato a delle tipiche query in SQL, immaginando come tabelle di database degli array di oggetti javascript creati con l'aiuto di mockaroo.
Rimando ad altri ottimi articoli, come questo, questo, questo o questo, la spiegazione dell'uso di map(), reduce(), filter(), sort(), Set(), l'operatore Spread […].
Puoi scaricare tutti i file da GitHub
OPERAZIONI FONDAMENTALI SULLE TABELLE
PROIEZIONE
La proiezione è un filtraggio sulle colonne
Ad esempio della tabella CLIENTI vogliamo visualizzare solo alcuni campi
-
SQL
SELECT id, companyName, country FROM clienti
-
Javascript
let proiez = clienti.map(c=>new Object({"id":c.id,"companyName": c.companyName, "country":c.country }));
SELEZIONE
la selezione è un filtraggio sulle righe
Della tabella CLIENTI vogliamo solo quelli provenienti dal Regno Unito
-
SQL
SELECT * FROM clienti WHERE country = "UK"
-
Javascript
let selez = clienti.filter(cc=>cc.country=="UK")
PRODOTTO CARTESIANO
il prodotto cartesiano fra due insiemi è dato da tutti gli elementi di A combinati con ogni elemento di B
-
JS
let a = ['a', 'b', 'c', 'd'] let b = [1, 2, 3] let prod = [].concat(...a.map(p => b.map(b => p + b))) /* [ 'a1', 'a2', 'a3', 'b1', 'b2', 'b3', 'c1', 'c2', 'c3', 'd1', 'd2', 'd3' */ ]
CLAUSOLA DISTINCT
Elimina i record uguali: in Javascript possiamo usare l'oggetto Set()
Elenco dei Paesi rappresentati
-
SQL
SELECT DISTINCT country FROM clienti ORDER BY country
-
Javascript
const distinct = Array.from(new Set(clienti.map(cc => cc.country))).sort()
SORTING
Per ordinare i record usiamo la funzione sort
-
SQL
SELECT * FROM clienti ORDER BY companyName
-
JS
let sorted = clienti.sort((a,b)=> ( b.companyName < a.companyName));
FUNZIONI DI AGGREGAZIONE
COUNT
Numero totale di clienti
-
SQL
SELECT COUNT(*) FROM clienti
-
JS
let numero_clienti = clienti.reduce((count, val) => count + 1, 0);
SUM
Totale prodotti in magazzino
-
SQL
SELECT SUM(unitsInStock) FROM prodotti
-
JS
let totp = prodotti.map(prod => prod.unitsInStock).reduce((sum, cc) => sum + cc, 0); //oppure let totp_alt = prodotti.reduce((sum, prod) => sum + prod.unitsInStock, 0);
MAX
Prezzo più alto
-
SQL
SELECT MAX(unitPrice) FROM prodotti
-
JS
const maxPrezzo = prodotti.map(cc => cc.unitPrice).reduce((max, d) => d > max ? d : max);
MIN
Prezzo più basso
-
SQL
SELECT MIN(unitPrice) FROM prodotti
-
JS
const minPrezzo = prodotti.map(cc => cc.unitPrice).reduce((min, d) => d < min ? d : min);
AVG
Prezzo medio
-
SQL
SELECT AVG(unitPrice) FROM prodotti
-
JS
const avgPrezzo = prodotti.map(cc => cc.unitPrice).reduce( (r, p) =>{ r.sum += p; ++r.count; return r }, { count: 0, sum: 0 });
TOP N
I dieci prodotti più costosi
-
SQL
SELECT TOP 10(unitPrice) FROM prodotti
-
JS
const topN = prodotti.sort( (a, b) => a.unitPrice - b.unitPrice ).reverse().slice(0, 10)
GROUP BY
Numero di prodotti per ogni categoria
-
SQL
SELECT categoryId, COUNT(*) FROM prodotti GROUP BY(categoryId)
-
JS
const groupByCategory = prodotti.reduce((gruppi, cc) => {
gruppi[cc.categoryId] = (gruppi[cc.categoryId] || 0) + 1;
return gruppi;
}, {})
Numero di clienti per Paese
-
SQL
SELECT country, COUNT(*) FROM CLIENTI GROUP BY(country)
-
JS
const groupClientiByCountry = clienti.reduce((gruppi, cc) => {gruppi[cc.country] = (gruppi[cc.country] || 0) + 1; return gruppi;}, {})
poi ordinare in base al numero di clienti
let filtra = Object.entries(groupByClientiCountry).map(kk => new Object({ c: kk[0], n: kk[1] })).sort((a, b) => b.n - a.n)
Numero di ordini per cliente (almeno 5)
-
SQL
SELECT customerId, count(*) FROM ordini GROUP BY customerId HAVING count(*) >= 5 ORDER BY count(*) DESC
-
JS
const OrdinigroupByCliente = Object.entries(ordini.reduce((gruppi, cc) => { gruppi[cc.customerId] = (gruppi[cc.customerId] || 0) + 1; return gruppi; }, {})) .map(kk => new Object({ c: kk[0], n: kk[1] })).sort((a, b) => b.n - a.n) .filter(o => o.n >= 5)
ALTRE FUNZIONI TIPICHE DI HADOOP
BINNING
Suddivide i record in base ad un criterio
Esempio: suddividere i prodotti in 4 categorie in base al prezzo
-
JS
const categ = prodotti.reduce((cate, cc) => { let pr = parseInt(cc.unitPrice) if (pr < 4) cate["molto_economici"].push(cc) else if (pr < 10) cate["economici"].push(cc) else if (pr < 50) cate["costosi"].push(cc) else cate["lusso"].push(cc) return cate; }, { molto_economici: [], economici: [], costosi: [], lusso: [] })
INDICE INVERTITO
Si usa per creare strutture tipo dizionario
Esempio 1: per ogni mese fornire l'elenco dei clienti che hanno effettuato almeno un ordine
-
JS
const invIndex = ordini.reduce((mesi, ord) => { let anno=new Date(ord.orderDate).getFullYear() let mese=new Date(ord.orderDate).getMonth() let key=mese+"-"+anno mesi[key] = (mesi[key] || new Set()).add(ord.customerId); return mesi; }, {})
Esempio 2: per ogni cliente fornire le date degli ordini
-
JS
const invIndex2 = ordini.reduce((date_ordini, cc) => { date_ordini[cc.customerId] = (date_ordini[cc.customerId] || new Set()).add(cc.orderDate.substr(0,10)); return date_ordini; }, {})
JOINS
Combinare due o più tabelle che si relazionano attraverso una o più colonne
LEFT OUTER JOIN: filtra tutti i valori della prima tabella anche se non hanno corrispondenza nella seconda tabella
-
SQL
SELECT * FROM prodotti LEFT JOIN categorie ON prodotti.categoryId = categorie.id;
-
JS
const left_join = prodotti.reduce((loj, pp) => { let lookup_categoria = categorie.filter(cc => pp.categoryId == cc.idCat); let union_rec if (lookup_categoria.length) union_rec = { ...pp, ...lookup_categoria[0] }; else union_rec = pp; loj.push(union_rec) return loj; } , [])
RIGHT OUTER JOIN: conserva tutti i valori della seconda tabella anche se non hanno corrispondenza nella prima
la Right Outer Join è equivalente alla Left Outer Join scambiando le tabelle
-
SQL
SELECT * FROM categorie LEFT OUTER JOIN prodotti ON categorie.id= prodotti.categoryId;
-
JS
const right_join = categorie.reduce((roj, cc) => { let lookup_prodotti = prodotti.filter(pp => pp.categoryId == cc.idCat); if (lookup_prodotti.length == 0) roj.push(cc); else { lookup_prodotti.map(v => { let union_rec = { ...cc, ...v } roj.push(union_rec) }) } return roj; }, [])
INNER JOIN (NATURAL JOIN)
Inner Join e Natural Join sono molto simili: cambia solo il numero di colonne restituite
Con questa soluzione facciamo prima il prodotto cartesiano tra le due tabelle e poi filtriamo solo i record con i valori della colonna "cerniera" uguali
-
SQL
SELECT * FROM prodotti, categorie WHERE prodotti.categoryId = categorie.id;
-
JS
let prod_cart = [].concat(...prodotti.map(pp => categorie.map(cc => Object.assign({}, { ...pp, ...cc })))) let natjoin = prod_cart.filter(f => (f.categoryId == f.idCat))
SUBQUERY
Esempio 1: elenco dei clienti che hanno fatto almeno un ordine
-
SQL
SELECT * FROM clienti WHERE id IN ( SELECT customerId FROM ordini)
-
JS
const elenco_clienti=clienti.filter(cl=>ordini.filter(oo=>oo.customerId=cl.idCli))
Esempio 2: elenco dei prodotti che sono stati ordinati almeno una volta in quantità superiore a 100 unità
-
SQL
SELECT DISTINCT idProd as id, nome as descr FROM prodotti WHERE productId IN (SELECT productId from dettagli WHERE quantity>100)
-
JS
let prod=dettagli.map(d=>d.prodotti.flat()).reduce((acc, val) => acc.concat(val), []). filter(v=>v.quantity>100).map(pp=>pp.productId) let proj=prodotti.filter(pp=>prod.includes(pp.idProd)) .map(p=>new Object({"id":p.idProd, "descr":p.name}))
Esempio 3: elenco degli ordini che comprendono prodotti di almeno due categorie diverse
-
SQL
SELECT Detttagli.OrderId, (SELECT COUNT( DISTINCT categoryId)) as ncat FROM Dettagli, Prodotti WHERE Dettagli.ProductId=Prodotti.ProductId GROUP BY Dettagli.OrderId HAVING ncat>1
-
JS
const groupByCat = (dettagli.reduce((gruppi, cc) => { var prod=[] cc.prodotti.forEach(pp=>prod.push(rispc[pp.productId])) let diversi=new Set(prod).size if(diversi>1) gruppi.push(new Object({"ord":cc.orderId, "det":prod.length, "div":diversi})); return gruppi; }, []))