sql >> Database >  >> RDS >> Mysql

Een tabel met voetbalgegevens van miljarden rijen partitioneren met behulp van gegevenscontext

In dit artikel leert u hoe u de semantiek achter uw gegevens kunt gebruiken wanneer u uw database partitioneert. Dit kan de prestaties van uw applicatie drastisch verbeteren. En, belangrijker nog, u zult ontdekken dat u uw partitioneringscriteria moet afstemmen op uw unieke toepassingsdomein.

Ik heb samengewerkt met een startup om een ​​web-app te ontwikkelen voor sportexperts om beslissingen te nemen en gegevens te verkennen. De applicatie ondersteunt elke sport, maar we zijn gevestigd in Europa - en Europeanen houden van voetbal. Elk van de honderden spellen die elke dag wereldwijd worden gespeeld, heeft duizenden rijen. In slechts een paar maanden tijd bereikte de tabel Evenementen in onze app een half miljard rijen!

Door te begrijpen hoe voetbalexperts onze gegevens opvragen, konden we de database intelligent partitioneren. De gemiddelde tijdverbetering op deze nieuwe tafel was tussen de 20x en 40x sneller. De gemiddelde tijdsverbetering op alle zoekopdrachten was 5x tot 10x.

Laten we ons nu verdiepen in dit scenario en leren waarom u uw gegevenscontext niet kunt negeren bij het partitioneren van een database.

De context presenteren

Onze sportapplicatie biedt zowel onbewerkte als geaggregeerde gegevens, hoewel de professionals die het hebben geadopteerd de voorkeur geven aan het laatste. De onderliggende database bevat terabytes aan complexe, ongestructureerde, heterogene data van verschillende providers. De grootste uitdaging was dus het ontwerpen van een betrouwbare, snelle en gemakkelijk te verkennen database.

Toepassingsdomein

In deze branche bieden veel providers hun klanten toegang tot de evenementen van de belangrijkste voetbalwedstrijden. Ze bieden u met name gegevens met betrekking tot wat er tijdens een wedstrijd is gebeurd, zoals doelpunten, assists, gele kaarten, passes en nog veel meer. De tabel met deze gegevens is verreweg de grootste waarmee we hebben moeten werken.

VPS-specificaties, technologieën en architectuur

Mijn team heeft de backend-applicatie ontwikkeld die de meest cruciale functies voor gegevensverkenning biedt. We gebruikten Kotlin v1.6 die draait op een JVM (Java Virtual Machine) als programmeertaal, Spring Boot 2.5.3 als framework en Hibernate 5.4.32.Final als de ORM (Object Relational Mapping). De belangrijkste reden waarom we voor deze technologiestack hebben gekozen, is dat snelheid een van de meest cruciale zakelijke vereisten is. We hadden dus een technologie nodig die gebruik kon maken van zware multi-thread-verwerking, en Spring Boot bleek een betrouwbare oplossing te zijn.

We hebben onze backend geïmplementeerd op een 16GB 8CPU VPS via een Docker-container die wordt beheerd door Dokku. Het kan maximaal 15 GB RAM gebruiken. Dit komt omdat één GB RAM is gereserveerd voor een op Redis gebaseerd cachingsysteem. We hebben het toegevoegd om de prestaties te verbeteren en te voorkomen dat de backend wordt overbelast met herhaalde bewerkingen.

Database- en tabelstructuur

Wat de database betreft, hebben we besloten te kiezen voor MySQL 8. Een VPS van 8 GB en 2 CPU's host momenteel de databaseserver, die tot 200 gelijktijdige verbindingen ondersteunt. De back-endtoepassing en de database bevinden zich in dezelfde serverfarm om communicatieoverhead te voorkomen. We hebben de databasestructuur ontworpen om duplicatie te voorkomen en met het oog op prestaties. We besloten een relationele database te gebruiken omdat we een consistente structuur wilden hebben om de gegevens die van de providers werden ontvangen, om te zetten. Op deze manier standaardiseren we de sportgegevens, waardoor het gemakkelijker wordt om ze te verkennen en te presenteren aan de eindgebruikers.

De database bevat honderden tabellen op het moment van schrijven en ik kan ze niet allemaal presenteren vanwege de NDA die ik heb ondertekend. Gelukkig is één tabel voldoende om grondig te analyseren waarom we uiteindelijk de op gegevenscontext gebaseerde partitie hebben aangenomen die u zo gaat zien. De echte uitdaging kwam toen we begonnen met het uitvoeren van zware zoekopdrachten op de Evenementen-tabel. Maar laten we, voordat we daarop ingaan, eens kijken hoe de tabel Evenementen eruitziet:

Zoals je kunt zien, zijn er niet veel kolommen, maar houd er rekening mee dat ik er een aantal heb moeten weglaten vanwege vertrouwelijkheid. Maar wat echt zaken hier zijn de parameterId en gameId kolommen. We gebruiken deze twee externe sleutels om een ​​type parameter te selecteren (bijv. doelpunt, gele kaart, pass, penalty) en de games waarin het gebeurde.

Prestatieproblemen

De Evenementen-tabel bereikte in slechts een paar maanden tijd een half miljard rijen. Zoals we in deze blogpost al uitgebreid hebben behandeld, is het grootste probleem dat we geaggregeerde bewerkingen moeten uitvoeren met behulp van langzame IN-query's. Dit komt omdat wat er tijdens een spel gebeurt niet zo belangrijk is. In plaats daarvan willen sportexperts geaggregeerde gegevens analyseren om trends te vinden en op basis daarvan beslissingen te nemen.

Ook, hoewel ze over het algemeen het hele seizoen of de laatste 5 of 10 wedstrijden analyseren, willen gebruikers vaak bepaalde spellen uitsluiten van hun analyse. Dit komt omdat ze niet willen dat een spel dat bijzonder slecht of goed wordt gespeeld, hun resultaten polariseert. We kunnen de geaggregeerde gegevens niet vooraf genereren omdat we dit voor alle mogelijke combinaties zouden moeten doen, wat niet haalbaar is. We moeten dus alle gegevens opslaan en direct samenvoegen.

Het prestatieprobleem begrijpen

Laten we nu ingaan op het centrale aspect dat leidde tot de prestatieproblemen waarmee we te maken hadden.

Tabellen met miljoen rijen zijn traag

Als je ooit te maken hebt gehad met tabellen met honderden miljoenen rijen, weet je dat ze inherent traag zijn. Je kunt er niet eens aan denken om JOINs op zulke grote tafels te draaien. Toch kunt u SELECT-query's binnen een redelijke tijd uitvoeren. Dit is met name het geval wanneer deze zoekopdrachten betrekking hebben op eenvoudige WHERE-voorwaarden. Aan de andere kant worden ze vreselijk traag bij het gebruik van aggregatiefuncties of IN-clausules. In deze gevallen kunnen ze gemakkelijk 80 seconden duren, wat gewoon te veel is.

Indices zijn niet genoeg

Om de prestaties te verbeteren, hebben we besloten enkele indexen te definiëren. Dit was onze eerste manier om een ​​oplossing te vinden voor de prestatieproblemen. Maar helaas leidde dit tot een ander probleem. Indexen kosten tijd en ruimte. Dit is over het algemeen onbeduidend, maar niet bij zulke grote tabellen. Het bleek dat het definiëren van complexe indexen op basis van de meest voorkomende zoekopdrachten enkele uren en GB's aan ruimte kostte. Indexen zijn ook nuttig, maar niet magisch.

Gegevenscontextgebaseerde databasepartitionering als oplossing

Omdat we het prestatieprobleem niet konden oplossen met op maat gemaakte indexen, besloten we een nieuwe aanpak te proberen. We spraken met andere experts, zochten online naar oplossingen, lazen artikelen op basis van vergelijkbare scenario's en besloten uiteindelijk dat het partitioneren van de database de juiste aanpak was om te volgen.

Waarom traditionele partitionering misschien niet de juiste aanpak is

Voordat we al onze grootste tabellen opdeelden, hebben we het onderwerp zowel in de officiële MySQL-documentatie als in interessante artikelen bestudeerd. Hoewel we het er allemaal over eens waren dat dit de juiste weg was, realiseerden we ons ook dat het een vergissing zou zijn om partitionering toe te passen zonder rekening te houden met ons specifieke toepassingsdomein. We begrepen met name hoe cruciaal het was om de juiste criteria te vinden bij het partitioneren van een database. Sommige experts op het gebied van partitionering hebben ons geleerd dat de traditionele benadering is om te partitioneren op het aantal rijen. Maar we wilden iets intelligenters en efficiënters vinden dan dat.

Duik in het applicatiedomein om de partitioneringscriteria te vinden

We hebben een essentiële les geleerd door het applicatiedomein te analyseren en onze gebruikers te interviewen. Sportexperts hebben de neiging om geaggregeerde gegevens van wedstrijden in dezelfde competitie te analyseren. Een voetbalcompetitie kan bijvoorbeeld een competitie, een toernooi of een enkele wedstrijd zijn waar je een trofee kunt winnen. Er zijn duizenden verschillende competities. De belangrijkste in Europa zijn de Champions League, Premier League, LaLiga, Serie A, Bundesliga, Eredivisie, Liga 1 en Primeira Liga.

Dit betekent dat onze gebruikers zeer zelden rekening houden met gegevens afkomstig van verschillende competities. Ook verkiezen ze de gegevens seizoen per seizoen te verkennen. Met andere woorden, ze verlaten zelden de context die wordt vertegenwoordigd door een sportcompetitie die in een bepaald seizoen wordt gespeeld. Onze databasestructuur drukte dit concept uit met een tabel genaamd SeasonCompetition , wiens doel het is om een ​​competitie te associëren met een specifiek seizoen. We realiseerden ons dus dat een goede aanpak zou zijn om onze grotere tabellen op te delen in subtabellen die verband houden met een bepaalde SeasonCompetition instantie.

We hebben specifiek de volgende naamindeling voor deze nieuwe tabellen gedefinieerd:<tableName>_<seasonCompetitionId> .

Als we dus 100 rijen hadden in de SeasonCompetition tabel, zouden we de grote Events . moeten splitsen tabel in de kleinere Events_1 , Events_2 , …, Events_100 tafels. Op basis van onze analyse zou deze aanpak in het gemiddelde geval leiden tot een aanzienlijke prestatieverbetering, hoewel in de zeldzaamste gevallen enige overhead zou worden geïntroduceerd.

De criteria overeenkomen met de meest voorkomende zoekopdrachten

Voordat we de scripts codeerden en lanceerden om deze complexe en potentieel retourloze bewerking uit te voeren, hebben we onze onderzoeken gevalideerd door te kijken naar de meest voorkomende vragen die door onze backend-applicatie worden uitgevoerd. Maar toen we dit deden, kwamen we erachter dat de overgrote meerderheid van de vragen alleen betrekking had op games die binnen een seizoenscompetitie werden gespeeld. Dit overtuigde ons ervan dat we gelijk hadden. Dus hebben we alle grote tabellen in de database gepartitioneerd met de zojuist gedefinieerde aanpak.


SELECT AVG('value') as 'value', SUM('minutes') as 'minutes'
FROM 'Events'
WHERE 'parameterId' = 15 AND 'gameId' IN(223,241,245,212,201,299,187,304,187,205)
GROUP BY 'teamId'

Laten we nu de voor- en nadelen van deze beslissing bestuderen.

Voordelen

  • Het uitvoeren van query's op een tabel met maximaal een half miljoen rijen is veel efficiënter dan op een tabel met een half miljard rijen, vooral als het gaat om geaggregeerde query's.
  • Kleinere tabellen zijn gemakkelijker te beheren en bij te werken. Het toevoegen van een kolom of index is qua tijd en ruimte niet eens vergelijkbaar met voorheen. Plus, elke SeasonCompetition is anders en vereist andere analyses. Bijgevolg kan het speciale kolommen en indexen vereisen, en de bovengenoemde partitionering stelt ons in staat om hier gemakkelijk mee om te gaan.
  • De provider kan sommige gegevens wijzigen. Dit dwingt ons om verwijder- en update-query's uit te voeren, die oneindig veel sneller zijn op zulke kleine tabellen. Bovendien betreffen ze altijd slechts enkele games van een bepaalde SeasonCompetition , dus we hoeven nu nog maar op één tafel te werken.

Nadelen

  • Voordat we een vraag stellen over deze subtabellen, moeten we de seasonCompetitionId weten geassocieerd met de games van belang. Dit komt omdat de seasonCompetitionId waarde wordt gebruikt in de tabelnaam. Daarom moet onze backend deze informatie ophalen voordat de query wordt uitgevoerd door naar de games in analyse te kijken, wat een kleine overhead vertegenwoordigt.
  • Als een zoekopdracht betrekking heeft op een reeks spellen waarbij veel SeasonCompetitions betrokken zijn , moet de backend-toepassing een query uitvoeren op elke subtabel. In deze gevallen kunnen we de gegevens dus niet langer aggregeren op databaseniveau en moeten we het op applicatieniveau doen. Dit introduceert enige complexiteit in de backend-logica. Tegelijkertijd kunnen we deze query's parallel uitvoeren. We kunnen de opgehaalde gegevens ook efficiënt en parallel samenvoegen.
  • Het beheren van een database met duizenden tabellen is niet eenvoudig en kan een uitdaging zijn om in een client te verkennen. Evenzo is het toevoegen van een nieuwe kolom of het bijwerken van een bestaande kolom in elke tabel omslachtig en vereist een aangepast script.

Effecten van gegevenscontextgebaseerde partitionering op prestaties

Laten we nu eens kijken naar de tijdverbetering die is bereikt bij het uitvoeren van een query in de nieuwe gepartitioneerde database.

  • Tijdverbetering in het gemiddelde geval (query met slechts één SeasonCompetition ):van 20x tot 40x
  • Tijdverbetering in het algemeen (query met betrekking tot een of meer SeasonCompetitions ):van 5x tot 10x

Laatste gedachten

Het partitioneren van uw database is ongetwijfeld een uitstekende manier om de prestaties te verbeteren, vooral bij grote databases. Als u dit echter doet zonder rekening te houden met uw specifieke toepassingsdomein, kan dit een vergissing zijn of tot een inefficiënte oplossing leiden. In plaats daarvan is het cruciaal om de tijd te nemen om het domein te bestuderen door experts en uw gebruikers te interviewen en door te kijken naar de meest uitgevoerde zoekopdrachten om zeer efficiënte partitioneringscriteria te bedenken. Dit artikel liet je zien hoe je dit kunt doen en demonstreerde de resultaten van een dergelijke aanpak aan de hand van een praktijkvoorbeeld.


  1. Fout bij het installeren van mysql2:kan de native extensie van de gem niet bouwen

  2. nulls gebruiken in een door mysqli voorbereide verklaring

  3. SQL Server:Tabel Meta-Data extraheren (beschrijving, velden en hun datatypes)

  4. Hoe GROUP BY te gebruiken om strings in SQL Server samen te voegen?