Solopher schreef op dinsdag 10 februari 2015 @ 13:12:
Ik heb laatst ongeveer hetzelfde gedaan, gebruik gemaakt van elasticsearch aggregations.
http://www.elasticsearch..../search-aggregations.html
Eerst haal ik al mijn aggregations op, hier staan ook de buckets in. Dit cache is wel in mijn intern geheugen met een duur van 5 minuten.
Vervolgens als er gezocht wordt houd ik twee soorten filters bij:
Orig en een TermsFilter.
Orig houd ik bij om nog te kunnen laten zien wat er eventueel in de andere categorieen zit als je maar 1 categorie selecteert. Zodra je een subselect in een categorie maakt dan worden de waarden in orig allemaal 0 behalve die van de geselecteerde hoofdcategorie.
Dit draait inmiddels een maand of 3 naar volle tevredenheid.
Dit lijkt mij ook een goede opzet voor mijn doel. Hoeveel records heb jij ongeveer en welke specs heeft de machine waarop alles draait?
cariolive23 schreef op dinsdag 10 februari 2015 @ 14:48:
12 miljoen records stelt niet zoveel voor, al ben je natuurlijk afhankelijk van de snelheid van de machine. IO en RAM zijn daarbij key.
Maar, wanneer ik iets lees over 40 kolommen in een MySQL-database, bekruipt mij al snel het gevoel dat er niet is genormaliseerd en daarbij dus ook niet is geoptimaliseerd voor een RDBMS. Ik ken jouw data niet, ik kan het dus niet zeker weten, maar ik ben wel benieuwd. Je kunt een schroef prima met een hamer in het hout slaan, maar het is niet optimaal. Over het algemeen bereik je met een schroevendraaier een veel beter resultaat.... Zo ook met een database, een RDBMS werkt het beste wanneer de data is genormaliseerd. Ook wanneer de hardware resources beperkt zijn.
Het is op dit moment inderdaad niet genormaliseerd. Ik ben aan het uitzoeken wat precies het handigste is om te doen en wat ik nou precies wil. Ik wil in ieder geval een zo uitgebreid mogelijke facetted search maken. Sommige facets worden misschien niet vaak gebruikt, maar het moet wel kunnen.
ACM schreef op woensdag 11 februari 2015 @ 20:23:
Hoewel ik met jullie eens ben dat 12 miljoen niet iets is waar MySQL per se enorm veel moeite heeft, is het nou niet bepaald een triviale hoeveelheid records.
Als je daarop queries doet die veel van de records aanraakt, moet je niet verbaasd zijn als dat queries van veel meer dan enkele millisecondes zijn. Zeker niet als je te maken hebt met wat complexere statements.
Indexen kunnen zoals gebruikelijk veel helpen, maar ook daarbij geldt dat 12 miljoen records geen triviale hoeveelheid is. Indexen zijn vooral zinvol als je daarmee tot een kleine subset van de records kunt komen, bijvoorbeeld alle records voor een bepaald id of zelfs specifiek het records behorende bij een primaire-sleutelwaarde.
Pas dan heb je pas echt baat bij de O(log-n) werking van b-trees enzo. In de hier genoemde scenario's heb je mogelijk vooral baat bij het feit dat de waardes er gesorteerd in staan en er minder I/O nodig is om alle data door te kijken.
Bij het bepalen van facet-statistieken wil je - neem ik aan - effectief voor elke kolom weten hoeveel records er gaan overblijven als je een te kiezen optie combineert met de reeds opgebouwde query. Die resultaten van je query krijgen kan relatief snel zijn door indexen te combineren, maar vervolgens daar weer alle mogelijke waarden van afleiden... daar zal MySQL toch echt over het algemeen alle resulterende records voor door moeten scannen, wat het automatisch geen heel triviale klus meer maakt.
Bovendien is dit typisch een query die in MySQL juist
trager kan worden zodra je velden begint te selecteren - zeker als ze nog altijd een groot deel van alle records opleveren - omdat dan minder efficient puur de indexen uitgelezen kunnen worden ('t voorbeeld uit de topicstart kan puur uit een index op 'field' worden afgeleid, een met een where-clause niet meer).
Met 40 kolommen betekent dat je die query uiteindelijk 40x moet herhalen, steeds met een andere grouping. Het zou mij eerlijk gezegd niet verbazen als onze databaseserver (2x Intel E5-2643 met 3,3Ghz cores, 256GB ram en SSD's) ook ruim boven de 1 seconde blijft uitkomen, zeker met de varianten die niet zo makkelijk puur op basis van indexen kunnen werken.
Ik merk in ieder geval een erg groot (enorm) verschil tussen WHERE clauses waar 1m rijen onder vallen en WHERE clauses waar je maar zo'n 1k resultaten terug krijgt. Dus alle rijen uit de query moeten doorlopen worden om vervolgens de unieke waardes te vinden.
Bij de facet-statistieken wil ik inderdaad na elke selectie alles updaten met de nieuwe beschikbare nummers.
Gomez12 schreef op woensdag 11 februari 2015 @ 21:16:
[...]
Maar dat is enkel relevant als je 40 kolommen wilt blijven hanteren.
Een alternatief zou bijv kunnen zijn om die 40 kolommen af te splitsen naar een 2e tabel met een 1:n(/40) verhouding waardoor je 40 query's vervangt door 1 query.
Je praat dan wel over maximaal 480 miljoen records (alhoewel ik betwijfel of alle 40 kolommen altijd gevuld zijn) maar je hebt maar 1 query en grote kans dat je mysql een veel beter query-execution en caching mechanisme kan hanteren.
Heb je verschillende data-types in die 40 kolommen dan ga je wat extra dingen krijgen, maar het principe blijft hetzelfde.
Oftewel je kan ook nog wel wat winnen door je data-model aan te passen zodat die beter past bij je uiteindelijke doel.
Maar op zichzelf zou ik de easy way out pakken en gewoon investeren in RAM en elasticsearch / solr ervoor inzetten, die hebben allerlei interne optimalisaties die je in mysql zelf zou moeten nabouwen.
Hier zat ik inderdaad ook naar te kijken, wordt gebruikt in deze
tutorial ook.
Alle 40 kolommen zijn niet altijd gevuld. Om iedereen nog een beter beeld te geven van de data: het gaat om RDW voertuig data. Ik heb dus een CSV bestandje met alle data, gebruikt dezelfde kolom namen als in de code hieronder. Behalve dan ID.
Dus ik ben vooral nog aan het expirementeren hoe ik alles het beste kan importeren, met welke tabellen structuur en welk DBMS om facetted search met zo veel mogelijk facetten te bouwen.
SQL:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
| CREATE TABLE voertuig (
ID INT AUTO_INCREMENT,
Aantalcilinders INT DEFAULT NULL,
Aantalstaanplaatsen INT DEFAULT NULL,
Aantalzitplaatsen INT DEFAULT NULL,
BPM INT DEFAULT NULL,
Brandstofverbruikbuitenweg FLOAT DEFAULT NULL,
Brandstofverbruikgecombineerd FLOAT DEFAULT NULL,
Brandstofverbruikstad FLOAT DEFAULT NULL,
Catalogusprijs FLOAT DEFAULT NULL,
Cilinderinhoud INT DEFAULT NULL,
CO2uitstootgecombineerd INT DEFAULT NULL,
Datumaanvangtenaamstelling DATETIME DEFAULT NULL,
DatumeersteafgifteNederland DATETIME DEFAULT NULL,
Datumeerstetoelating DATETIME DEFAULT NULL,
Eerstekleur VARCHAR(255) DEFAULT NULL,
G3installatie VARCHAR(255) DEFAULT NULL,
Handelsbenaming VARCHAR(255) DEFAULT NULL,
Hoofdbrandstof VARCHAR(255) DEFAULT NULL,
Inrichting VARCHAR(255) DEFAULT NULL,
Kenteken VARCHAR(255) DEFAULT NULL,
Laadvermogen INT DEFAULT NULL,
Massaleegvoertuig INT DEFAULT NULL,
Massarijklaar INT DEFAULT NULL,
Maximaleconstructiesnelheid INT DEFAULT NULL,
Maximumtetrekkenmassaautonoomgeremd INT DEFAULT NULL,
Maximumtetrekkenmassageremd INT DEFAULT NULL,
Maximumtetrekkenmassamiddenasgeremd INT DEFAULT NULL,
Maximumtetrekkenmassaongeremd INT DEFAULT NULL,
Maximumtetrekkenmassaopleggergeremd INT DEFAULT NULL,
Merk VARCHAR(255) DEFAULT NULL,
Milieuclassificatie VARCHAR(255) DEFAULT NULL,
Nevenbrandstof VARCHAR(255) DEFAULT NULL,
Retrofitroetfilter VARCHAR(255) DEFAULT NULL,
Toegestanemaximummassavoertuig INT DEFAULT NULL,
Tweedekleur VARCHAR(255) DEFAULT NULL,
Vermogen INT DEFAULT NULL,
Vermogenbromsnorfiets INT DEFAULT NULL,
VervaldatumAPK DATETIME DEFAULT NULL,
Voertuigsoort VARCHAR(255) DEFAULT NULL,
Wachtopkeuren VARCHAR(255) DEFAULT NULL,
WAMverzekerdgeregistreerd VARCHAR(255) DEFAULT NULL,
Zuinigheidslabel VARCHAR(255) DEFAULT NULL,
PRIMARY KEY (id),
INDEX Aantalcilinders (Aantalcilinders),
INDEX Aantalstaanplaatsen (Aantalstaanplaatsen),
INDEX Aantalzitplaatsen (Aantalzitplaatsen),
INDEX BPM (BPM),
INDEX Brandstofverbruikgecombineerd (Brandstofverbruikgecombineerd),
INDEX Brandstofverbruikstad (Brandstofverbruikstad),
INDEX Catalogusprijs (Catalogusprijs),
INDEX Cilinderinhoud (Cilinderinhoud),
INDEX CO2uitstootgecombineerd (CO2uitstootgecombineerd),
INDEX Datumaanvangtenaamstelling (Datumaanvangtenaamstelling),
INDEX DatumeersteafgifteNederland (DatumeersteafgifteNederland),
INDEX Datumeerstetoelating (Datumeerstetoelating),
INDEX Eerstekleur (Eerstekleur),
INDEX G3installatie (G3installatie),
INDEX Handelsbenaming (Handelsbenaming),
INDEX Hoofdbrandstof (Hoofdbrandstof),
INDEX Inrichting (Inrichting),
INDEX Kenteken (Kenteken),
INDEX Laadvermogen (Laadvermogen),
INDEX Massaleegvoertuig (Massaleegvoertuig),
INDEX Massarijklaar (Massarijklaar),
INDEX Maximaleconstructiesnelheid (Maximaleconstructiesnelheid),
INDEX Maximumtetrekkenmassaautonoomgeremd (Maximumtetrekkenmassaautonoomgeremd),
INDEX Maximumtetrekkenmassageremd (Maximumtetrekkenmassageremd),
INDEX Maximumtetrekkenmassamiddenasgeremd (Maximumtetrekkenmassamiddenasgeremd),
INDEX Maximumtetrekkenmassaongeremd (Maximumtetrekkenmassaongeremd),
INDEX Maximumtetrekkenmassaopleggergeremd (Maximumtetrekkenmassaopleggergeremd),
INDEX Merk (Merk),
INDEX Milieuclassificatie (Milieuclassificatie),
INDEX Nevenbrandstof (Nevenbrandstof),
INDEX Retrofitroetfilter (Retrofitroetfilter),
INDEX Toegestanemaximummassavoertuig (Toegestanemaximummassavoertuig),
INDEX Tweedekleur (Tweedekleur),
INDEX Vermogen (Vermogen),
INDEX Vermogenbromsnorfiets (Vermogenbromsnorfiets),
INDEX VervaldatumAPK (VervaldatumAPK),
INDEX Voertuigsoort (Voertuigsoort),
INDEX Wachtopkeuren (Wachtopkeuren),
INDEX WAMverzekerdgeregistreerd (WAMverzekerdgeregistreerd),
INDEX Zuinigheidslabel (Zuinigheidslabel)
) |
Wat ik uiteindelijk dus wil is:
1. Voertuig informatie weergeven op kenteken
2. API call om voertuig gegevens op te halen
3. Facetted search op de homepage om voertuigen te vinden die voldoen aan de gekozen facet opties
4. Facetted search API call zodat ik deze ook kan opbouwen in een Android en iOS app
Nummer 1 en 2 zijn af. De Android app staat ook in de Playstore. Heb hem van de grond af aangebouwd en gebruikt nu de RDW API. Maar ik wil toch minder afhankelijk zijn van derde partijen en ben nu dit alles dus in elkaar in het knutselen. Het geavanceerd zoeken kan ook niet met de RDW API. Alleen op één kolom met de exacte waarde. Mercedes op kolom MERK vind je niks, Mercedes-Benz wel.
In ieder geval de hardware eisen onderschat. Ik dacht dat 12 mln records niet heel veel voorstelde. Het selecteren van kentekens gaat ook zonder problemen. Maar het tellen van unieke combinaties met nog een aantal WHERE clauses is gewoon niet efficient met de huidige opzet. Moet alle rijen in de result set doorlopen en dan de uniques er uit filteren eigenlijk. Dus duurt met een query zoals
SQL:
1
| SELECT Voertuigsoort FROM voertuig WHERE Inrichting='Hatchback' GROUP BY Voertuigsoort; |
4 rows returned, 41.226 sec
SQL:
1
| SELECT Handelsbenaming FROM voertuig WHERE Merk='Mazda' GROUP BY Handelsbenaming; |
1000 rows returned, 16.538 sec
Als je de GROUP BY achterwege laat voor de unieke waardes duurt het nog maar 0.185s.
Zoiets als
SQL:
1
| SELECT * FROM voertuig WHERE Kenteken='22TZZB'; |
Duurt slechts 0.050s zoals je ook zou verwachten.
SQL:
1
| SELECT DISTINCT Nevenbrandstof FROM voertuig; |
Unieke waardes zonder WHERE clause duurt ook slechts 0.035s
Het probleem is ten eerste dus het vinden van de unieke waardes met WHERE clause en ten tweede ook nog de counts er bij. Dus het oplossen hiervan: meer RAM, betere structuur/normaliseren, mogelijk ander DBMS... in ieder geval interessant en best lastig.