sql >> Databasteknik >  >> RDS >> Mysql

Dela upp en miljardradstabell med fotbollsdata med hjälp av datakontext

I den här artikeln kommer du att lära dig hur du använder semantiken bakom dina data när du partitionerar din databas. Detta kan drastiskt förbättra din applikations prestanda. Och, viktigast av allt, kommer du att upptäcka att du bör skräddarsy dina partitioneringskriterier för din unika applikationsdomän.

Jag har samarbetat med en startup för att utveckla en webbapp för sportexperter att fatta beslut och utforska data. Applikationen stöder alla sporter, men vi är baserade i Europa - och européer älskar fotboll. Vart och ett av de hundratals spel som spelas varje dag över hela världen kommer med tusentals rader. På bara några månader nådde tabellen Händelser i vår app en halv miljard rader!

Genom att förstå hur fotbollsexperter sökte efter våra data kunde vi partitionera databasen intelligent. Den genomsnittliga tidsförbättringen på detta nya bord var mellan 20x och 40x snabbare. Den genomsnittliga tidsförbättringen för alla frågor var 5X till 10X.

Låt oss nu fördjupa oss i det här scenariot och lära oss varför du inte kan ignorera din datakontext när du partitionerar en databas.

Presentera sammanhanget

Vår sportapplikation erbjuder både rå och aggregerad data, även om de proffs som anammat det föredrar det senare. Den underliggande databasen innehåller terabyte av komplexa, ostrukturerade, heterogena data från flera leverantörer. Så den största utmaningen var att designa en pålitlig, snabb och lätt att utforska databas.

Applikationsdomän

I den här branschen erbjuder många leverantörer sina kunder tillgång till evenemangen i de viktigaste fotbollsspelen. Specifikt ger de dig data relaterad till vad som hände under en match, såsom mål, assist, gula kort, passningar och mycket mer. Tabellen som innehåller dessa data är den överlägset största vi hade att arbeta med.

VPS-specifikationer, teknologier och arkitektur

Mitt team har utvecklat backend-applikationen som tillhandahåller de mest avgörande funktionerna för datautforskning. Vi antog Kotlin v1.6 som körs ovanpå en JVM (Java Virtual Machine) som programmeringsspråk, Spring Boot 2.5.3 som ramverk och Hibernate 5.4.32.Final som ORM (Object Relational Mapping). Den främsta anledningen till att vi valde denna teknikstack är att hastighet är ett av de mest avgörande affärskraven. Så vi behövde en teknik som kunde utnyttja tung flertrådsbearbetning, och Spring Boot visade sig vara en pålitlig lösning.

Vi distribuerade vår backend på en 16GB 8CPU VPS genom en Docker-behållare som hanteras av Dokku. Den kan använda 15 GB RAM som mest. Detta beror på att en GB RAM är dedikerad till ett Redis-baserat cachningssystem. Vi lade till det för att förbättra prestandan och undvika att överbelasta backend med upprepade operationer.

Databas och tabellstruktur

När det gäller databasen bestämde vi oss för att välja MySQL 8. En 8GB och 2 CPU VPS är för närvarande värd för databasservern, som stöder upp till 200 samtidiga anslutningar. Backend-applikationen och databasen finns i samma serverfarm för att undvika kommunikationsoverhead. Vi designade databasstrukturen för att undvika dubbelarbete och med prestanda i åtanke. Vi bestämde oss för att anta en relationsdatabas eftersom vi ville ha en konsekvent struktur för att konvertera data som mottagits från leverantörerna. På så sätt standardiserar vi sportdata, vilket gör det lättare att utforska och presentera det för slutanvändarna.

Databasen innehåller hundratals tabeller i skrivande stund, och jag kan inte presentera dem alla på grund av den NDA jag undertecknade. Lyckligtvis räcker det med en tabell för att noggrant analysera varför vi slutade anta den datakontextbaserade partition du är på väg att se. Den verkliga utmaningen kom när vi började utföra tunga frågor på tabellen Händelser. Men innan vi går in i det, låt oss se hur tabellen Händelser ser ut:

Som ni ser så handlar det inte om många spalter, men tänk på att jag var tvungen att utelämna några av dem av sekretesskäl. Men vad egentligen saker här är parameterId och gameId kolumner. Vi använder dessa två främmande nycklar för att välja en typ av parameter (t.ex. mål, gult kort, passning, straff) och de matcher där det hände.

Prestandaproblem

Händelsetabellen nådde en halv miljard rader på bara några månader. Som vi redan har behandlat på djupet i det här blogginlägget är huvudproblemet att vi måste utföra aggregerade operationer med hjälp av långsamma IN-frågor. Detta beror på att det som händer under ett spel inte är så viktigt. Istället vill sportexperter analysera aggregerad data för att hitta trender och fatta beslut baserat på dem.

Även om de i allmänhet analyserar hela säsongen eller de senaste 5 eller 10 matcherna, vill användare ofta utesluta vissa spel från sin analys. Detta beror på att de inte vill att ett spel som spelas särskilt dåligt eller bra för att polarisera sina resultat. Vi kan inte förgenerera aggregerad data eftersom vi skulle behöva göra detta på alla möjliga kombinationer, vilket inte är genomförbart. Så vi måste lagra all data och aggregera den i farten.

Förstå prestandaproblemet

Låt oss nu dyka in i den centrala aspekten som ledde till de prestationsproblem som vi hade att möta.

Tabeller med miljoner rader är långsamma

Om du någonsin har hanterat tabeller som innehåller hundratals miljoner rader, vet du att de är långsamma. Du kan inte ens tänka på att köra JOINs på så stora bord. Ändå kan du utföra SELECT-frågor inom rimlig tid. Detta gäller särskilt när dessa frågor involverar enkla WHERE-villkor. Å andra sidan blir de fruktansvärt långsamma när man använder aggregerade funktioner eller IN-satser. I dessa fall kan de lätt ta upp till 80 sekunder, vilket helt enkelt är för mycket.

Index räcker inte

För att förbättra prestandan bestämde vi oss för att definiera några index. Detta var vårt första sätt att hitta en lösning på prestandaproblemen. Men tyvärr ledde detta till ett annat problem. Index tar tid och utrymme. Detta är i allmänhet obetydligt, men inte när man har att göra med så stora bord. Det visade sig att det tog flera timmar och GB utrymme att definiera komplexa index baserat på de vanligaste frågorna. Index är också användbara men är inte magiska.

Datakontextbaserad databaspartitionering som en lösning

Eftersom vi inte kunde lösa prestandaproblemet med specialdefinierade index, bestämde vi oss för att prova ett nytt tillvägagångssätt. Vi pratade med andra experter, letade online efter lösningar, läste artiklar baserade på liknande scenarier och beslutade till slut att partitionering av databasen var rätt tillvägagångssätt.

Varför traditionell partitionering kanske inte är rätt tillvägagångssätt

Innan vi partitionerade alla våra största tabeller studerade vi ämnet både i den officiella MySQL-dokumentationen och i intressanta artiklar. Även om vi alla var överens om att detta var rätt väg att gå, insåg vi också att det skulle vara ett misstag att tillämpa partitionering utan att ta hänsyn till vår specifika applikationsdomän. Specifikt förstod vi hur viktigt det var att hitta de rätta kriterierna när man partitionerar en databas. Vissa experter på partitionering lärde oss att den traditionella metoden är att partitionera på antalet rader. Men vi ville hitta något mer intelligent och effektivare än så.

Går in i applikationsdomänen för att hitta partitioneringskriterierna

Vi lärde oss en viktig läxa genom att analysera applikationsdomänen och intervjua våra användare. Sportexperter tenderar att analysera samlad data från spel i samma tävling. Till exempel kan en tävling i fotboll vara en liga, en turnering eller en enda match där du kan vinna en trofé. Det finns tusentals olika tävlingar. De viktigaste i Europa är Champions League, Premier League, LaLiga, Serie A, Bundesliga, Eredivisie, Liga 1 och Primeira Liga.

Detta innebär att våra användare tar hänsyn till data som kommer från olika tävlingar mycket sällan. Dessutom föredrar de att utforska data säsong för säsong. Med andra ord, de lämnar sällan det sammanhang som representeras av en sporttävling som spelas under en viss säsong. Vår databasstruktur uttryckte detta koncept med en tabell som heter SeasonCompetition , vars mål är att associera en tävling med en specifik säsong. Så vi insåg att ett bra tillvägagångssätt skulle vara att dela upp våra större tabeller i undertabeller relaterade till en viss SeasonCompetition instans.

Specifikt definierade vi följande namnformat för dessa nya tabeller:<tableName>_<seasonCompetitionId> .

Följaktligen, om vi hade 100 rader i SeasonCompetition tabell, skulle vi behöva dela upp de stora Events tabellen till den mindre Events_1 , Events_2 , …, Events_100 tabeller. Baserat på vår analys skulle detta tillvägagångssätt leda till en avsevärd prestandaökning i det genomsnittliga fallet, även om det skulle införa vissa overhead i de mest sällsynta fallen.

Matcha kriterierna med de vanligaste frågorna

Innan vi kodade och startade skripten för att utföra denna komplexa och potentiellt returfria operation, validerade vi våra studier genom att titta på de vanligaste frågorna som utförs av vår backend-applikation. Men när vi gjorde det fick vi reda på att den stora majoriteten av frågorna bara gällde spel som spelades inom en säsongstävling. Detta övertygade oss om att vi hade rätt. Så vi partitionerade alla stora tabeller i databasen med det tillvägagångssätt som just definierats.


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'

Låt oss nu studera för- och nackdelarna med detta beslut.

Proffs

  • Att köra frågor i en tabell som innehåller högst en halv miljon rader är mycket mer prestanda än att göra det på en tabell med en halv miljard rader, särskilt när det kommer till aggregerade frågor.
  • Mindre tabeller är lättare att hantera och uppdatera. Att lägga till en kolumn eller index är inte ens jämförbart med tidigare när det gäller tid och rum. Dessutom varje SeasonCompetition är annorlunda och kräver olika analyser. Följaktligen kan det kräva speciella kolumner och index, och den tidigare nämnda partitioneringen gör att vi enkelt kan hantera detta.
  • Leverantören kan ändra vissa uppgifter. Detta tvingar oss att utföra raderings- och uppdateringsfrågor, som är oändligt mycket snabbare på så små tabeller. Dessutom gäller de alltid bara vissa spel i en viss SeasonCompetition , så vi behöver bara arbeta på ett enda bord nu.

Nackdelar

  • Innan vi gör en fråga om dessa undertabeller måste vi känna till seasonCompetitionId kopplade till de intressanta spelen. Detta beror på att seasonCompetitionId värde används i tabellnamnet. Därför måste vår backend hämta denna information innan frågan körs genom att titta på spelen i analys, vilket representerar en liten overhead.
  • När en fråga involverar en uppsättning spel som involverar många SeasonCompetitions , måste backend-applikationen köra en fråga på varje undertabell. Så i dessa fall kan vi inte längre aggregera data på databasnivå, och vi måste göra det på applikationsnivå. Detta introducerar viss komplexitet i backend-logiken. Samtidigt kan vi utföra dessa frågor parallellt. Dessutom kan vi aggregera den hämtade datan effektivt och parallellt.
  • Hantera en databas med tusentals tabeller är inte lätt och kan vara utmanande att utforska i en klient. På samma sätt är det besvärligt att lägga till en ny kolumn eller uppdatera en befintlig kolumn i varje tabell och kräver ett anpassat skript.

Effekter av datakontextbaserad partitionering på prestanda

Låt oss nu titta på tidsförbättringen som uppnås när en fråga körs i den nya partitionerade databasen.

  • Tidsförbättring i det genomsnittliga fallet (fråga som endast involverar en SeasonCompetition ):från 20x till 40x
  • Tidsförbättring i det allmänna fallet (fråga som involverar en eller flera SeasonCompetitions ):från 5x till 10x

Sista tankar

Att partitionera din databas är utan tvekan ett utmärkt sätt att förbättra prestandan, särskilt på stora databaser. Men att göra det utan att ta hänsyn till din specifika applikationsdomän kan vara ett misstag eller leda till en ineffektiv lösning. Istället är det avgörande att ta dig tid att studera domänen genom att intervjua experter och dina användare och titta på de mest utförda frågorna för att skapa mycket effektiva partitioneringskriterier. Den här artikeln visade hur du gör detta och visade resultaten av ett sådant tillvägagångssätt genom en verklig fallstudie.


  1. Uppdatera fråga med hjälp av Subquery i SQL Server

  2. PostgreSQL:Visa tabeller i PostgreSQL

  3. Hur väljer man den första raden för varje grupp i MySQL?

  4. Byt namn på en tabell i SQL Server (T-SQL)