Igapäevase töö ajal kasutan Entity Frameworkit. See on väga mugav, kuid mõnel juhul on selle jõudlus aeglane. Vaatamata sellele, et EF-i jõudluse parandamise kohta on palju häid artikleid, antakse väga häid ja kasulikke nõuandeid (nt vältige keerulisi päringuid, parameetreid Jätke vahele ja kasutage, kasutage vaateid, valige ainult vajalikud väljad jne), kuid mitte nii palju tõesti teha, kui peate kasutama keerukat Contains
kahel või enamal väljal - teisisõnu, kui ühendate andmed mälu loendiga .
Kontrollime järgmist näidet:
var localData = GetDataFromApiOrUser(); var query = from p in context.Prices join s in context.Securities on p.SecurityId equals s.SecurityId join t in localData on new { s.Ticker, p.TradedOn, p.PriceSourceId } equals new { t.Ticker, t.TradedOn, t.PriceSourceId } select p; var result = query.ToList();
Ülaltoodud kood ei tööta EF 6-s üldse ja kuigi see töötab teeb töötab EF Core'is, toimub liitumine tegelikult kohapeal - kuna minu andmebaasis on kümme miljonit kirjet, kõik neist laetakse alla ja kogu mälu kulub ära. See pole EF-i viga. On oodata. Kas poleks siiski fantastiline, kui selle lahendamiseks oleks midagi? Selles artiklis kavatsen selle jõudluse kitsaskoha kõrvaldamiseks teha mõne erineva lähenemisega katse.
Proovin selle saavutamiseks erinevaid viise, alustades kõige lihtsamatest ja edasijõudnutest. Igal sammul esitan koodi ja mõõdikud, nagu kulutatud aeg ja mälukasutus. Pange tähele, et katkestan võrdlusuuringute programmi käitamise, kui see töötab kauem kui kümme minutit.
Võrdlusuuringute programmi kood asub järgmises hoidla . See kasutab C #, .NET Core, EF Core ja PostgreSQL. Kasutasin masinat, millel oli Intel Core i5, 8 GB RAM ja SSD.
Testimiseks mõeldud DB skeem näeb välja selline:
Alustamiseks proovime midagi lihtsat.
var result = new List(); using (var context = CreateContext()) { foreach (var testElement in TestData) { result.AddRange(context.Prices.Where( x => x.Security.Ticker == testElement.Ticker && x.TradedOn == testElement.TradedOn && x.PriceSourceId == testElement.PriceSourceId)); } }
Algoritm on lihtne: leidke iga testiandmete elemendi kohta andmebaasist sobiv element ja lisage see tulemuste kogu. Sellel koodil on ainult üks eelis: seda on väga lihtne rakendada. Samuti on see loetav ja hooldatav. Selle ilmne puudus on see, et see on kõige aeglasem. Kuigi kõik kolm veergu on indekseeritud, loob võrguside üldkulud siiski jõudluse kitsaskoha. Siin on mõõdikud:
mis on /c/
Niisiis, suure mahu jaoks kulub umbes üks minut. Mälu tarbimine näib olevat mõistlik.
Proovime nüüd lisada koodile paralleelsust. Põhiidee on see, et andmebaasi paralleelsete lõimade tabamine võib parandada üldist jõudlust.
var result = new ConcurrentBag(); var partitioner = Partitioner.Create(0, TestData.Count); Parallel.ForEach(partitioner, range => { var subList = TestData.Skip(range.Item1) .Take(range.Item2 - range.Item1) .ToList(); using (var context = CreateContext()) { foreach (var testElement in subList) { var query = context.Prices.Where( x => x.Security.Ticker == testElement.Ticker && x.TradedOn == testElement.TradedOn && x.PriceSourceId == testElement.PriceSourceId); foreach (var el in query) { result.Add(el); } } } });
Huvitav on see, et väiksemate katseandmekogumite puhul töötab see lähenemisviis aeglasemalt kui esimene lahendus, kuid suuremate proovide puhul on see kiirem (antud juhul umbes 2 korda). Mälu tarbimine muutub veidi, kuid mitte oluliselt.
Proovime teist lähenemist:
var result = new List(); using (var context = CreateContext()) { var tickers = TestData.Select(x => x.Ticker).Distinct().ToList(); var dates = TestData.Select(x => x.TradedOn).Distinct().ToList(); var ps = TestData.Select(x => x.PriceSourceId) .Distinct().ToList(); var data = context.Prices .Where(x => tickers.Contains(x.Security.Ticker) && dates.Contains(x.TradedOn) && ps.Contains(x.PriceSourceId)) .Select(x => new { x.PriceSourceId, Price = x, Ticker = x.Security.Ticker, }) .ToList(); var lookup = data.ToLookup(x => $'{x.Ticker}, {x.Price.TradedOn}, {x.PriceSourceId}'); foreach (var el in TestData) { var key = $'{el.Ticker}, {el.TradedOn}, {el.PriceSourceId}'; result.AddRange(lookup[key].Select(x => x.Price)); } }
See lähenemine on problemaatiline. Käivitamise aeg sõltub väga andmetest. See võib hankida just nõutavad kirjed (sel juhul on see väga kiire), kuid võib-olla tagastab palju rohkem (võib-olla isegi 100 korda rohkem).
Vaatleme järgmisi testiandmeid:
Siin küsin 2018-01-01 vahetatud Ticker1 ja 02.01.2018 kaubeldavate Ticker1 hindade kohta. Siiski tagastatakse tegelikult neli kirjet.
Ticker
Unikaalsed väärtused on Ticker1
ja Ticker2
. TradedOn
Unikaalsed väärtused on 2018-01-01
ja 2018-01-02
.
php teisendab Unicode'i ASCII-ks
Neli kirjet vastavad sellele väljendile.
Sellepärast on vaja kohalikku ülevaatust ja miks selline lähenemine on ohtlik. Mõõdikud on järgmised:
Kohutav mälu tarbimine! Suurte mahtudega testid nurjusid 10-minutilise ajalõpu tõttu.
Muutkem paradigmat: ehitame vana hea Expression
iga katseandmekogumi kohta.
var result = new List(); using (var context = CreateContext()) { var baseQuery = from p in context.Prices join s in context.Securities on p.SecurityId equals s.SecurityId select new TestData() { Ticker = s.Ticker, TradedOn = p.TradedOn, PriceSourceId = p.PriceSourceId, PriceObject = p }; var tradedOnProperty = typeof(TestData).GetProperty('TradedOn'); var priceSourceIdProperty = typeof(TestData).GetProperty('PriceSourceId'); var tickerProperty = typeof(TestData).GetProperty('Ticker'); var paramExpression = Expression.Parameter(typeof(TestData)); Expression wholeClause = null; foreach (var td in TestData) { var elementClause = Expression.AndAlso( Expression.Equal( Expression.MakeMemberAccess( paramExpression, tradedOnProperty), Expression.Constant(td.TradedOn) ), Expression.AndAlso( Expression.Equal( Expression.MakeMemberAccess( paramExpression, priceSourceIdProperty), Expression.Constant(td.PriceSourceId) ), Expression.Equal( Expression.MakeMemberAccess( paramExpression, tickerProperty), Expression.Constant(td.Ticker)) )); if (wholeClause == null) wholeClause = elementClause; else wholeClause = Expression.OrElse(wholeClause, elementClause); } var query = baseQuery.Where( (Expression)Expression.Lambda( wholeClause, paramExpression)).Select(x => x.PriceObject); result.AddRange(query); }
Saadud kood on üsna keeruline. Väljendite loomine pole kõige lihtsam asi ja hõlmab mõtlemist (mis iseenesest ei ole nii kiire). Kuid see aitab meil luua ühe päringu, kasutades palju … (.. AND .. AND ..) OR (.. AND .. AND ..) OR (.. AND .. AND ..) ...
Need on tulemused:
Isegi hullem kui kumbki eelnev lähenemine.
Proovime veel ühte lähenemist:
aws-lahendused arhitekti eksami ettevalmistus
Lisasin andmebaasi uue tabeli, mis hoiab päringu andmeid. Iga päringu puhul saan nüüd:
var result = new List(); using (var context = CreateContext()) { context.Database.BeginTransaction(); var reducedData = TestData.Select(x => new SharedQueryModel() { PriceSourceId = x.PriceSourceId, Ticker = x.Ticker, TradedOn = x.TradedOn }).ToList(); // Here query data is stored to shared table context.QueryDataShared.AddRange(reducedData); context.SaveChanges(); var query = from p in context.Prices join s in context.Securities on p.SecurityId equals s.SecurityId join t in context.QueryDataShared on new { s.Ticker, p.TradedOn, p.PriceSourceId } equals new { t.Ticker, t.TradedOn, t.PriceSourceId } select p; result.AddRange(query); context.Database.RollbackTransaction(); }
Kõigepealt mõõdikud:
Tulemus on väga hea. Väga kiiresti. Hea on ka mälu tarbimine. Kuid puudused on:
Kuid peale selle on selline lähenemine tore - kiire ja loetav. Ja päringuplaan on sel juhul vahemälus!
Kasutan siin NuGeti paketti nimega EntityFrameworkCore.MemoryJoin . Hoolimata asjaolust, et selle nimes on sõna Core, toetab see ka EF 6. Seda nimetatakse MemoryJoiniks, kuid tegelikult saadab see määratud päringuandmed VÄÄRTUSEKS serverisse ja kogu töö tehakse SQL-serveris.
Kontrollime koodi.
var result = new List(); using (var context = CreateContext()) { // better to select needed properties only, for better performance var reducedData = TestData.Select(x => new { x.Ticker, x.TradedOn, x.PriceSourceId }).ToList(); var queryable = context.FromLocalList(reducedData); var query = from p in context.Prices join s in context.Securities on p.SecurityId equals s.SecurityId join t in queryable on new { s.Ticker, p.TradedOn, p.PriceSourceId } equals new { t.Ticker, t.TradedOn, t.PriceSourceId } select p; result.AddRange(query); }
Mõõdikud:
parimad programmeerimiskeeled robootika jaoks
See näeb vinge välja. Kolm korda kiirem kui eelmine lähenemisviis - see muudab selle seni kõige kiiremaks. 3,5 sekundit 64K-plaatide jaoks! Kood on lihtne ja arusaadav. See töötab kirjutuskaitstud koopiatega. Kontrollime kolme elemendi jaoks loodud päringut:
SELECT 'p'.'PriceId', 'p'.'ClosePrice', 'p'.'OpenPrice', 'p'.'PriceSourceId', 'p'.'SecurityId', 'p'.'TradedOn', 't'.'Ticker', 't'.'TradedOn', 't'.'PriceSourceId' FROM 'Price' AS 'p' INNER JOIN 'Security' AS 's' ON 'p'.'SecurityId' = 's'.'SecurityId' INNER JOIN ( SELECT 'x'.'string1' AS 'Ticker', 'x'.'date1' AS 'TradedOn', CAST('x'.'long1' AS int4) AS 'PriceSourceId' FROM ( SELECT * FROM ( VALUES (1, @__gen_q_p0, @__gen_q_p1, @__gen_q_p2), (2, @__gen_q_p3, @__gen_q_p4, @__gen_q_p5), (3, @__gen_q_p6, @__gen_q_p7, @__gen_q_p8) ) AS __gen_query_data__ (id, string1, date1, long1) ) AS 'x' ) AS 't' ON (('s'.'Ticker' = 't'.'Ticker') AND ('p'.'PriceSourceId' = 't'.'PriceSourceId')
Nagu näete, edastatakse tegelikud väärtused seekord mälust SQL-serverisse VALUES-konstruktsioonis. Ja see teeb trikki: SQL-server suutis teha kiire liitumisoperatsiooni ja kasutada indekse õigesti.
Siiski on mõned puudused (võite lugeda rohkem minu kohta Ajaveeb ):
Siinkohal, mida olen siin testinud, valiksin kindlasti MemoryJoin'i. Keegi teine võib vaidlustada, et puudused on ületamatud, ja kuna kõiki neid ei saa praegu lahendada, peaksime hoiduma laienduse kasutamisest. Noh, minu jaoks on see nagu öelda, et te ei tohiks nuga kasutada, sest võite ennast lõigata. Optimeerimine oli ülesanne mitte noorematele arendajatele, vaid kellelegi, kes saab aru, kuidas EF töötab. Selleks võib see tööriist jõudlust dramaatiliselt parandada. Kes teab? Võib-olla ühel päeval lisab keegi Microsofti dünaamiliste VÄÄRTUSTE põhitoe.
Lõpuks, siin on veel mõned skeemid tulemuste võrdlemiseks.
Allpool on skeem toimingu tegemiseks kulunud aja kohta. MemoryJoin on ainus, kes teeb tööd mõistliku aja jooksul. Suuri mahtusid saab töödelda ainult nelja lähenemisviisiga: kaks naiivset teostust, jagatud tabel ja MemoryJoin.
mis on cfo funktsioon?
Järgmine skeem on mõeldud mälu tarbimiseks. Kõik lähenemised näitavad enam-vähem samu numbreid, välja arvatud see, millel on mitu Contains
Seda nähtust kirjeldati eespool.
DBSet on abstraktsioon, mis on sõna otseses mõttes tabelisse salvestatud objektide kogum (tavaliselt laisalt koormatud). DBSetis tehtud toimingud tehakse tegelikult tegelike andmebaasikirjetega SQL-päringute kaudu.
Entity Framework on objekti relatsioonilise kaardistamise raamistik, mis pakub standardset liidest (erinevate tarnijate) relatsiooniandmebaasidesse salvestatud andmetele juurdepääsuks.
Koodipõhine lähenemine tähendab, et arendaja loob mudeli klassid enne tegeliku DB loomist. Üks suurimaid eeliseid on andmebaasimudeli salvestamine allika juhtimissüsteemidesse.