Kdy byste měli zvážit indexování SQL tabulky?




Kolik záznamů by měl být před považuji indexování mé sql tabulky?
Neexistuje žádný dobrý důvod vzdát zjevné indexy (FKS, atd), při vytváření tabulky. To se nikdy výrazně ovlivnit výkon na to, aby nepotřebné indexy malých tabulek, a to je dobré, aby se první řez, když je vaše mysl v souvislosti s návrhem schématu. Také některé indexy slouží k zabránění duplicity, které mohou být užitečné bez ohledu na velikost tabulky.


Myslím, že správná odpověď na vaši otázku je, že počet záznamů v tabulce by měl mít nic společného s tím, když se vytvořit indexy.
<p>protože ostatní odpovědi již naznačují, sql server může nebo nemusí explicitně zajistit, že řádky jsou seřazeny v pořadí seskupených indexů před <code>insert</code>.</p><p>to závisí na tom, zda má klastrovaný indexový operátor v plánu sadu vlastností <code>dmlrequestsort</code> (což zase závisí na odhadovaném počtu řádků, které jsou vloženy).</p><p>pokud zjistíte, že sql server to z jakéhokoli důvodu podceňuje, můžete mít prospěch z přidání explicitního <code>order by</code> k dotazu <code>select</code> k minimalizaci rozdělení stránek a následné fragmentaci z operace <code>insert</code></p><p><strong> příklad: </strong></p><pre><code>use tempdb; go create table t(n int primary key,filler char(2000)) create table t2(n int primary key,filler char(2000)) go declare @t table (u uniqueidentifier primary key default newid(),n int) insert into @t(n) select number from master..spt\_values where type = 'p' and number between 0 and 499 /*estimated row count wrong as inserting from table variable*/ insert into t(n) select t1.n*1000 + t2.n from @t t1, @t t2 /*same operation using explicit sort*/ insert into t2(n) select t1.n*1000 + t2.n from @t t1, @t t2 order by t1.n*1000 + t2.n select avg\_fragmentation\_in\_percent, fragment\_count, page\_count, avg\_page\_space\_used\_in\_percent, record\_count from sys.dm\_db\_index\_physical\_stats(2, object\_id('t'), null, null, 'detailed') ; select avg\_fragmentation\_in\_percent, fragment\_count, page\_count, avg\_page\_space\_used\_in\_percent, record\_count from sys.dm\_db\_index\_physical\_stats(2, object\_id('t2'), null, null, 'detailed') ; </code></pre><p>ukazuje, že <code>t</code> je masivně roztříštěný</p><pre><code>avg\_fragmentation\_in\_percent fragment\_count page\_count avg\_page\_space\_used\_in\_percent record\_count      99.3116118225536 92535 92535 67.1668272794663 250000 99.5 200 200 74.2868173956017 92535 0 1 1 32.0978502594514 200 </code></pre><p>ale pro <code>t2</code> fragmentace je minimální</p><pre><code>avg\_fragmentation\_in\_percent fragment\_count page\_count avg\_page\_space\_used\_in\_percent record\_count      0.376 262 62500 99.456387447492 250000 2.1551724137931 232 232 43.2438349394613 62500 0 1 1 37.2374598468001 232 </code></pre><p>naopak někdy budete chtít donutit sql server, aby podceňoval počet řádků, když víte, že data jsou již předem roztříděna a chcete zabránit zbytečnému třídění. jedním z pozoruhodných příkladů je vložení velkého počtu řádků do tabulky pomocí seskupeného indexového klíče <code>newsequentialid</code>. <a href="https://connect.microsoft.com/sqlserver/feedback/details/668540/unnecessary-sort-operation-for-insert-into-clustered-index-with-newsequentialid-key">ve verzích sql serveru před denali sql server přidává zbytečnou a potenciálně nákladnou operaci řazení</a> . tomu lze zabránit</p><pre><code>declare @var int =2147483647 insert into foo select top (@var) * from bar </code></pre><p>sql server poté odhadne, že bude vloženo 100 řádků bez ohledu na velikost <code>bar</code>, která je pod prahem, při kterém je do plánu přidáno řazení. jak je však uvedeno v poznámkách níže, znamená to, že vložka bohužel nebude moci využít výhody minimálního protokolování.</p> <p>nedávno jsem měl něco podobného s tabulkou 3,5 milionu řádků. moje aktualizace by nikdy nedokončila. po mnoha experimentech a frustrace jsem konečně našel viníka. ukázalo se, že se jedná o indexy v aktualizované tabulce.</p><p>řešením bylo zrušit všechny indexy v aktualizované tabulce před spuštěním příkazu aktualizace. jakmile jsem to udělal, aktualizace skončila za pár minut. po dokončení aktualizace jsem znovu vytvořil indexy a vrátil se zpět do práce. pravděpodobně vám to v tuto chvíli nepomůže, ale možná někdo, kdo hledá odpovědi.</p><p>nechal bych indexy na stole, ze kterého stahujete data. ten nebude muset nadále aktualizovat žádné indexy a měl by pomoci s hledáním dat, která chcete aktualizovat. na pomalém notebooku to fungovalo dobře.</p> <p>na vaši otázku neexistuje jednoduchá odpověď, ale zde je několik věcí, na které byste se měli zamyslet.</p><p>zaprvé, měřítko není jediná věc, o kterou se musíme starat. co děláte s vašimi daty je. pokud máte 500 tabulek 30 tb dat a děláte jednoduché oltp s velmi malým počtem hlášení), nemyslím si, že budete mít příliš mnoho problémů "tam jsou 32tb databáze na postgresql tam venku. zároveň se však výkon poněkud sníží, protože to musí zasáhnout disk na všechno. podobně, pokud máte 50 tb, pokud data, ale mají běžně zasaženou sadu asi 100 gb, pak můžete postavte server s dostatečným množstvím ram), abyste tuto část db udrželi v paměti a jste zlatí.</p><p>na druhou stranu, pokud se pokoušíte režim (nejběžnější hodnotu) z 1tb dat, nezáleží na tom, jaký systém používáte, bude to <em> bolestivé </em> s nebo bez střepů. (edit: sharding může ve skutečnosti tento problém zhoršit <em>. </em>)</p><p>mezi hlavní problémy, s nimiž se setkáte s obrovskými db na mysql a postgresql, patří skutečnost, že ani nepodporuje paralelismus dotazování. jinými slovy, dotaz je spuštěn jako jeden blok jedním vláknem a nelze jej rozdělit na kousky a spustit samostatně. toto je nejčastěji problém při spouštění velkých analytických dotazů na velké množství dat. to je místo, kde postgres-xc a green plum přicházejí k záchraně, protože oddělují úložiště od provádění, a mohou tak učinit na úrovni koordinátora. všimněte si, že postgres-xc a green plum v zásadě používají střepiny interně, ale koordinátoři prosazují veškerou konzistenci globálně.</p><p>s paralelizací dotazování můžete dotaz rozdělit, mít různé procesory/diskové i/o kanály spouštět jeho části a vykazovat zpětné části sady výsledků, které mají být sestaveny, a předány zpět do aplikace. to je opět velmi užitečné při analytickém namísto zpracování transakcí.</p><p>druhou věcí je, že některé systémy, například vertica nebo greenplum, uchovávají sloupce informací společně. to ztěžuje použití systému z perspektivy oltp) a snižuje tam výkon, ale drasticky zvyšuje výkon pro velké analytické pracovní zatížení. jedná se tedy o kompromis specifický pro pracovní vytížení.</p><p>odpověď je, že jakmile se dostanete nad 1-2 tb ve velikosti, kterou můžete <em>, můžete </em>, ocitnout se tváří v tvář číslu kompromisů mezi systémy a pracovním zatížením. opět je to specifické pro databáze, velikost pracovních sad atd. nicméně v tomto bodě musíte opravdu jít se systémy sněhové vločky, tj. systémy jedinečné a přizpůsobené vašemu pracovnímu zatížení.</p><p>to samozřejmě znamená, že limity nejsou obecně kvantifikovatelné.</p><p><strong>pravit</strong>: nyní jsem pracoval s 9tb databází, která zpracovává směs pracovní podpory při rozhodování a transakčního zpracování v postgresql. jedinou největší výzvou je, že pokud máte otázky, které zasáhly velkou část sady dat, budete muset chvíli počkat na odpověď.</p><p>nicméně s pečlivou pozorností k základům (včetně indexů, autovaku, jak tyto fungují na nízké úrovni atd.) a dostatečným výpočetním prostředkům, jsou zcela zvládnutelné (a odhaduji, že by dobře zvládnutelné do rozsahu 30tb v pg).</p><p><strong>edit2</strong>: jakmile se vydáte na 100 tb, i když to, co bude fungovat, bude záviset na vašem datovém souboru. momentálně pracuji na jednom, který se do tohoto rozsahu nevejde, protože to nejprve zasáhne limit 32 tb v tabulce v postgresql.</p> <p>tady je to, co jsem udělal předtím:</p><pre><code>(select 'tablea', * from tablea except select 'tablea', * from tableb) union all (select 'tableb', * from tableb except select 'tableb', * from tablea) </code></pre><p>funguje to dost dobře na stolech, které mají asi 1 000 000 řádků, ale nejsem si jistý, jak dobře by to fungovalo na extrémně velkých stolech.</p><p><strong>přidáno:</strong></p><p>spustil jsem dotaz proti svému systému, který porovnává dvě tabulky s 21 poli běžných typů ve dvou různých databázích připojených ke stejnému serveru se systémem sql server 2005. tabulka obsahuje asi 3 miliony řádků a liší se přibližně 25 000 řádků. primární klíč v tabulce je však zvláštní, protože se jedná o složený klíč z 10 polí (je to kontrolní tabulka).</p><p>plány provádění dotazů mají celkové náklady 184,255879 pro <code>union</code> a 184,228383 pro <code>union all</code>. cena stromu se liší pouze v posledním kroku před vrácením řádků, zřetězením.</p><p>skutečné provedení jednoho dotazu trvá asi 42 s plus asi 3 s, než se řádky skutečně přenesou. čas mezi dvěma dotazy je stejný.</p><p><strong>druhý dodatek:</strong></p><p>toto je ve skutečnosti <em> extrémně </em> rychlé, každý z nich běží proti 3 milionům řádků přibližně za 2,5 s:</p><pre><code>select checksum\_agg(binary\_checksum(*)) from tablea select checksum\_agg(binary\_checksum(*)) from tableb </code></pre><p>pokud se výsledky těchto neshodují, víte, že tabulky jsou odlišné. pokud se však výsledky <em> shodují </em>, nemáte <em> </em> zaručeno, že tabulky jsou totožné kvůli [velmi nepravděpodobné] možnosti kolize kontrolního součtu.</p><p>nejsem si jistý, jak změny datového typu mezi tabulkami ovlivní tento výpočet. spustil bych dotaz proti zobrazením <code>system</code> nebo <code>information\_schema</code> zobrazení.</p><p>zkoušel jsem dotaz proti jiné tabulce s 5 miliony řádků a ten běžel asi za 5 s, takže se zdá být převážně o (n).</p> <p>provozoval jsem systém s nejlepší částí z tisíce databází rozložených na více serverech. všechny byly identické struktury a byly synchronizovány s databází šablon, která byla na každém počítači.</p><p>to mi umožnilo migrovat databáze z jednoho db na druhý, pokud by se jeden příliš přetěžoval, a když se změnil klientský mix, mohl jsem na různých serverech vytvářet nové databáze, abych mohl načíst rovnováhu mezi servery. to byla největší výhoda, kterou jsem získal od systému, v tom, že jsem měl na několika samostatných serverech několik velkých kusů cínu, které prováděly více komplikovaných dotazů současně.</p><p>skvělá věc je, že můžete do konfigurace přidat servery svou vlastní rychlostí, protože každý server se začíná přetížit, přidat další do mixu, přenést některé dbs na nový server a skončit pěkně načíst vyváženou sadu serverů. opravdu pěkný a jednoduchý způsob, jak přidat měřítko do systému, kdykoli je to potřeba!</p><p>důvodem, proč jsem šel s tímto přístupem spíše než s jediným obrovským databázovým přístupem, byla naprostá velikost potenciální databáze, která by byla vytvořena ... každá z 1000 databází měla 200 tabulek a mnoho jednotlivých tabulek v každé z databáze obsahovaly mnoho stovek <strong>milionů</strong> řádků dat!</p><p>konfigurace jediné databáze by vyžadovala, aby určité tabulky (přibližně 8 z nich) měly více miliard řádků dat a celková velikost db by byla přes 10tb. podařilo se nám mít více serverů s 5tb úložiště raid 10, s mnoha databázemi na každém.</p><p>to bych udělal! doufám, že to pomůže vašemu rozhodování ... :)</p> <p>zatímco smršťování je skutečně nebezpečné z důvodů uvedených zde. mezi jimboho odpovědí a johnovou odpovědí existuje šťastné médium ... vždy byste měli vážně zvážit, zda chcete databázi zmenšit či nikoli.</p><p>v ideálním světě - vytvořili byste svou databázi se spoustou volného místa, do kterého byste se mohli rozrůst. tuto databázi nazývám „správná velikost“. umožnili byste, aby byl tento volný prostor k dispozici, a nesnažte se jej vrátit a udržet svou celkovou velikost přímo na použité velikosti. proč? protože vaše databáze nakonec poroste znovu. pak se znovu zmenšíte .. a budete uvíznuti v tomto strašném vzoru zbytečných smršťování následovaných růsty - a celou dobu, jak několik poukázalo, budete zvýšení fragmentace indexu.</p><p>blogoval jsem o tom, kde jsem napomenul lidi na " <a href="http://www.straightpathsql.com/archives/2009/01/dont-touch-that-shrink-button/">nedotýkejte se toho zmenšovacího tlačítka!</a> ", ale někdy ... někdy musíte. pokud máte rozsáhlou databázi, právě jste uvolnili významný prostor a neočekáváte, že se do ní někdy vrátí - dobře, pak je v pořádku zvážit zmenšení jako jednorázovou operaci, pokud se budete moci postarat o fragmentaci vašeho indexu později přestavbou jim. operace smršťování může být časově náročná, takže byste ji měli naplánovat na dobu, kdy můžete zaplatit tuto cenu běžícího smršťování. postup vytváření prázdné db a kopírování dat do něj funguje - ale to se může stát velmi obtížným u větších databází a mnoha dat.</p><p>pokud plánujete přidat tento prostor zpět do db pomocí běžného využití a růstových vzorců do budoucna, pak možná budete chtít místo ponechat.</p><p><strong> také </strong> řekli jste, že jste „vymazali“ svůj protokol transakcí. byl bych zvědavý, jak jste to udělali, ale když čtete příspěvek, který jsem sdílel, a ostatní v řadě uvidíte několik tipů na správu protokolu transakcí. ale zkrátka - pokud jste v režimu úplného zotavení, měli byste brát pravidelné zálohy protokolu, abyste si udrželi opakované použití protokolu. v opačném případě - bez záloh protokolu v režimu full - soubor protokolu neustále roste a roste a roste a vždy ukládá to, co jste udělali, protože jste řekli sql, že nechcete jen udržovat tento protokol pro zotavení po havárii, ale chcete zachovat jeho ruční zálohování pro přehrání transakcí/vrácení transakcí pro obnovení do určitého bodu v čase pro účely obnovy ... pokud jste v jednoduchosti a vidíte, jak se protokol rozrůstá, může to být znak (obvykle), že děláte lot práce v jedné transakci (ať už jste řekli <code>begin tran ... do work.... commit tran</code> nebo zda jste právě vydali jeden velký příkaz <code>delete</code> a odstranili jste celou nepořádek dat v jedné implicitní transakci.)</p><p>předpokládám také, že hledáte toto volné místo ve vašem systému souborů. pokud to hledáte v rámci sql a v tak velkém souboru, který máte - mohlo by to být tak, že čekáte na vyčištění ducha, aby se dokončilo, pokud se budete dívat ihned po operaci. <a href="http://www.sqlskills.com/blogs/paul/post/inside-the-storage-engine-ghost-cleanup-in-depth.aspx">paul randal blogy o ghost cleanup</a> .</p> <p>můžete použít rss kanál z twitteru, ukládat záznamy do databáze (překročit limit 24/72 hodin) a použít #hashtag nebo podobný, u kterého můžete zkontrolovat, zda má být zveřejněn.</p><p>jak často kontrolujete zdroj, jak vyčistíte položky databáze a kolik # hashagů používáte, záleží na vás a kolik času máte.</p> <p>v plánech provádění byste se neměli příliš spoléhat na procenta nákladů. jedná se o <em> vždy odhadované náklady</em>, a to i v plánech po provedení s „skutečnými“ čísly pro věci, jako je počet řádků. odhadované náklady jsou založeny na modelu, který funguje velmi dobře pro účel, pro který je určen: umožnění optimalizátoru zvolit mezi různými plány provádění kandidátů pro stejný dotaz. informace o nákladech jsou zajímavé a faktor, který je třeba zvážit, ale zřídka by měla být primární metrikou pro ladění dotazů. interpretace informací plánu provádění vyžaduje širší pohled na prezentovaná data.</p><p><strong> itemtran clustered index seek operator </strong></p><p><img src="https://i.stack.imgur.com/lvet6.jpg" alt="itemtran clustered index seek"></p><p>tento operátor jsou skutečně dvě operace v jednom. nejprve operace vyhledávání indexu najde všechny řádky, které odpovídají predikátu <code>product\_code\_v42 = 'm10bolt'</code>, pak každý řádek použije zbytkový predikát <code>bit\_field\_v41 &amp; 4 = 0</code>. dochází k implicitní konverzi <code>bit\_field\_v41</code> z jejího základního typu (<code>tinyint</code> nebo <code>smallint</code>) na <code>integer</code>.</p><p>k převodu dochází, protože <a href="https://msdn.microsoft.com/en-us/library/ms174965.aspx">bitový operátor and</a> (&amp;) vyžaduje, aby oba operandy byly stejného typu. implicitní typ konstantní hodnoty '4' je celé číslo a <a href="https://msdn.microsoft.com/en-us/library/ms190309.aspx">pravidla priority datového typ</a> znamená, že je převedena hodnota pole <code>bit\_field\_v41</code> s nižší prioritou.</p><p>problém (jako je) se snadno opraví zápisem predikátu jako <code>bit\_field\_v41 &amp; convert(tinyint, 4) = 0</code> - což znamená, že konstantní hodnota má nižší prioritu a je převedena (během konstantního skládání) spíše než hodnota sloupce. pokud je <code>bit\_field\_v41</code> <code>tinyint</code>, nedojde k žádné konverzi. podobně lze použít funkci <code>convert(smallint, 4)</code>, pokud <code>bit\_field\_v41</code> je <code>smallint</code>. to znamená, že v tomto případě <em> převod není problém s výkonem</em>, ale stále je dobrým zvykem porovnávat typy a vyhýbat se implicitním převodům tam, kde je to možné.</p><p>hlavní část odhadovaných nákladů na toto hledání je až do velikosti základní tabulky. zatímco seskupený klíč indexu je sám o sobě poměrně úzký, velikost každého řádku je velká. definice tabulky není uvedena, ale pouze sloupce použité v pohledu přidávají významnou šířku řádku. protože seskupený index zahrnuje všechny sloupce, vzdálenost mezi seskupenými klíči indexu je šířka <em> řádek</em>, nikoli šířka <em> indexové klíče</em>. použití přípon přípon verzí v některých sloupcích naznačuje, že skutečná tabulka obsahuje ještě více sloupců pro předchozí verze.</p><p>když se podíváme na sloupce vyhledávání, zbytkového predikátu a výstupu, výkon tohoto operátora lze zkontrolovat izolovaně vytvořením ekvivalentního dotazu (<code>1 &lt;&gt; 2</code> je trik, který zabraňuje auto-parametrizaci, rozpor je odstraněn optimalizátorem a nezobrazí se v plánu dotazů):</p><pre><code>select it.booking\_no\_v32, it.qtycheckedout, it.qtyreturned, it.trans\_qty, it.trans\_type\_v41 from dbo.tblitemtran as it where 1 &lt;&gt; 2 and it.product\_code\_v42 = 'm10bolt' and it.bit\_field\_v41 &amp; convert(tinyint, 4) = 0; </code></pre><p>výkon tohoto dotazu se studenou datovou mezipamětí je zajímavý, protože čtení (forward) by bylo ovlivněno fragmentací tabulky (seskupený index). klastrovací klíč pro tuto tabulku vyzývá k fragmentaci, takže by mohlo být důležité tento index pravidelně udržovat (reorganizovat nebo znovu sestavovat) a pomocí vhodného <code>fillfactor</code> umožnit místo pro nové řádky mezi okny údržby indexů.</p><p>provedl jsem test účinku fragmentace na čtení předem pomocí vzorových dat generovaných pomocí <a href="https://www.red-gate.com/products/sql-development/sql-data-generator/">sql data generator</a> . při použití stejného počtu řádků tabulky, jaké jsou uvedeny v plánu dotazů na otázku, vyústil vysoce fragmentovaný klastrovaný index do <code>select * from view</code> po 15 sekundách po <code>dbcc dropcleanbuffers</code>. stejný test za stejných podmínek s čerstvě obnoveným seskupeným indexem v tabulce itemtrans byl dokončen za 3 sekundy.</p><p>pokud jsou data tabulky obvykle zcela v mezipaměti, je problém fragmentace mnohem méně důležitý. ale i při nízké fragmentaci mohou široké řádky tabulky znamenat, že počet logických a fyzických čtení je mnohem vyšší, než by se dalo očekávat. můžete také experimentovat s přidáním a odebráním explicitního <code>convert</code>, abyste potvrdili mé očekávání, že implicitní problém s převodem zde není důležitý, s výjimkou porušení doporučených postupů.</p><p>více k tomuto bodu je odhadovaný počet řádků opouštějících operátora vyhledávání. odhad času optimalizace je 165 řádků, ale v době provedení bylo vyrobeno 4 226 kusů. k tomuto bodu se vracím později, ale hlavním důvodem nesrovnalosti je to, že selektivita zbytkového predikátu (zahrnující bitový-and) je pro optimalizátor velmi obtížná předvídat - ve skutečnosti se uchyluje k hádání.</p><p><strong> operátor filtru </strong></p><p><img src="https://i.stack.imgur.com/mayng.jpg" alt="filter operator"></p><p>zobrazuji zde predikát filtru většinou, abych ilustroval, jak jsou dva seznamy <code>not in</code> kombinovány, zjednodušeny a poté rozšířeny, a také poskytuji odkaz pro následující diskusi o hašovacích zápasech. testovací dotaz z požadavku lze rozšířit tak, aby zahrnoval jeho účinky a určoval vliv operátora filtru na výkon:</p><pre><code>select it.booking\_no\_v32, it.trans\_type\_v41, it.trans\_qty, it.qtyreturned, it.qtycheckedout from dbo.tblitemtran as it where it.product\_code\_v42 = 'm10bolt' and it.bit\_field\_v41 &amp; convert(tinyint, 4) = 0 and ( ( it.trans\_type\_v41 not in (2, 3, 6, 7, 18, 19, 20, 21, 12, 13, 22) and it.trans\_type\_v41 not in (6, 7) ) or ( it.trans\_type\_v41 not in (6, 7) ) or ( it.trans\_type\_v41 in (6, 7) and it.qtycheckedout = 0 ) or ( it.trans\_type\_v41 in (6, 7) and it.qtycheckedout &gt; 0 and it.trans\_qty - (it.qtycheckedout - it.qtyreturned) &gt; 0 ) ); </code></pre><p>operátor compute scalar v plánu definuje následující výraz (samotný výpočet je odložen, dokud není výsledek vyžadován pozdějším operátorem):</p><pre><code>[expr1016] = (trans\_qty - (qtycheckedout - qtyreturned)) </code></pre><p><strong> hash match operator </strong></p><p>provádění spojení na datových typech znaků není důvodem vysokých odhadovaných nákladů tohoto operátora. popis ssms zobrazuje pouze položku hash keys probe probe, ale důležité detaily jsou v okně ssms properties.</p><p>operátor hash match vytvoří hašovací tabulku s použitím hodnot ve sloupci <code>booking\_no\_v32</code> (build hash keys!) z tabulky itemtran a poté zkoumá shody pomocí sloupce <code>booking\_no</code> (hash keys probe) od tabulka rezervace. popisek ssms by také normálně zobrazoval zbytky sondy, ale text je příliš dlouhý na popis a je jednoduše vynechán.</p><p>rezidua sondy je podobná zbytku pozorovanému po hledání indexu dříve; zbytkový predikát je vyhodnocen na všech řádcích, které hash odpovídají, aby se určilo, zda by měl být řádek předán nadřazenému operátorovi. nalezení hash zápasů v dobře vyvážené hashovací tabulce je extrémně rychlé, ale použití komplexního zbytkového predikátu na každý řádek, který odpovídá, je při porovnání poměrně pomalé. popisek hash match v aplikaci plan explorer zobrazuje podrobnosti, včetně zbytkového výrazu probe:</p><p><img src="https://i.stack.imgur.com/c1nkp.jpg" alt="hash match operator"></p><p>zbytkový predikát je složitý a zahrnuje kontrolu stavu průběhu rezervace, když je sloupec k dispozici v tabulce rezervací. popisek také ukazuje stejný nesoulad mezi odhadovanými a skutečnými počty řádků viděnými dříve v hledání indexu. může se zdát podivné, že většina filtrování se provádí dvakrát, ale je to jen optimistický optimalizátor. neočekává, že části filtru, které lze posunout dolů ze zbytku sondy, odstraní všechny řádky (odhady počtu řádků jsou před a za filtrem stejné), ale optimalizátor ví, že by to mohlo být špatné. pravděpodobnost brzkého filtrování řádků (snížení nákladů na spojení hash) stojí za malou cenu extra filtru. celý filtr nelze posunout dolů, protože obsahuje test na sloupci z tabulky rezervací, ale většinou to může být. úplný test je stále vyžadován v testu zbytkové hodnoty hash pro správnost.</p><p>podceňování počtu řádků je problém pro operátora hash match, protože množství paměti rezervované pro hashovací tabulku je založeno na odhadovaném počtu řádků. pokud je paměť příliš malá pro velikost hashovací tabulky požadované v době běhu (kvůli většímu počtu řádků), hashova tabulka <strong> rekurzivně rozlévá </strong> na fyzickou <em> tempdb</em> úložiště, což často vede k velmi slabému výkonu. v nejhorším případě prováděcí motor zastaví rekurzivní rozlití hashovacích kbelíků a uchytí se k algoritmu <em> velmi pomalý</em> rozlití hash (rekurzivní nebo záchranná opatření) je nejpravděpodobnější příčinou problémů s výkonem nastíněných v otázce (nikoli sloupců spojení typu nebo implicitních konverzí). hlavní příčinou by byl server rezervující příliš málo paměti pro dotaz na základě nesprávného odhadu počtu řádků (kardinality).</p><p>je smutné, že před sql server 2012 není v plánu provádění naznačeno, že operace hašování překročila přidělení paměti (které nemůže být dynamicky růst po rezervování před spuštěním spuštění, i když má server velké množství volné paměti) a musela se přelít do tempdb. je možné monitorovat <a href="https://msdn.microsoft.com/en-us/library/ms190736.aspx">hash warning event class</a> pomocí profiler, ale může být obtížné korelovat varování s konkrétním dotazem.</p><p><strong> oprava problémů </strong></p><p>těmito třemi problémy jsou fragmentace, zbytková složka sondy v operátorovi hašovací shody a nesprávný odhad mohutnosti vyplývající z hádání při hledání indexu.</p><p><strong> doporučené řešení </strong></p><p>zkontrolujte fragmentaci a v případě potřeby ji opravte, naplánujte údržbu tak, aby index zůstal přijatelně organizovaný. obvyklý způsob, jak opravit odhad kardinality, je poskytnout statistiku. v takovém případě optimalizátor potřebuje statistiku kombinace (<code>product\_code\_v42</code>, <code>bitfield\_v41 &amp; 4 = 0</code>). nemůžeme vytvořit statistiky výrazu přímo, takže musíme nejprve vytvořit vypočítaný sloupec pro výraz bitového pole a poté vytvořit manuální statistiku více sloupců:</p><pre><code>alter table dbo.tblitemtran add bit3 as bit\_field\_v41 &amp; convert(tinyint, 4); create statistics [stats dbo.itemtran (product\_code\_v42, bit3)] on dbo.tblitemtran (product\_code\_v42, bit3); </code></pre><p>definice vypočteného sloupcového textu se musí přesně shodovat s textem v definici pohledu, aby se statistika mohla použít, takže by se mělo korigovat zobrazení, aby se vyloučila implicitní konverze, a současně by se mělo dbát na to, aby byla zajištěna shoda textu.</p><p>statistiky ve více sloupcích by měly vést k mnohem lepším odhadům, což výrazně snižuje pravděpodobnost, že operátor hashovací metody použije rekurzivní rozlití nebo algoritmus výpomoci. přidání vypočítaného sloupce (což je operace pouze s metadaty a nezabere žádné místo v tabulce, protože není označeno <code>persisted</code>) a statistika více sloupců je můj nejlepší odhad při prvním řešení.</p><p>při řešení problémů s výkonem dotazů je důležité měřit věci jako uplynulý čas, využití procesoru, logická čtení, fyzická čtení, typy čekání a doby trvání atd. atd. může být také užitečné spustit části dotazu samostatně pro ověření podezření na příčiny, jak je uvedeno výše.</p><p>v některých prostředích, kde není nejdůležitější pohled na data, může být užitečné spustit proces na pozadí, který zhmotňuje celý pohled do tabulky snímků každou tak často. tato tabulka je pouze normální základní tabulka a lze ji indexovat pro čtení dotazů, aniž byste se museli starat o dopad na výkon aktualizace.</p><p><strong> zobrazit indexování </strong></p><p>nenechte se v pokušení přímo indexovat původní pohled. výkon čtení bude úžasně rychlý (jediné hledání v pohledu indexu), ale (v tomto případě) budou všechny problémy s výkonem ve stávajících plánech dotazů přeneseny do dotazů, které upravují všechny sloupce tabulky, na které se v pohledu odkazuje. dotazy, které mění řádky základní tabulky, budou skutečně ovlivněny velmi špatně.</p><p><strong> pokročilé řešení s částečným indexovaným pohledem </strong></p><p>pro tento konkrétní dotaz existuje částečné řešení indexovaného pohledu, které opravuje odhady kardinality a odstraňuje zbytky filtru a sondy, ale je založeno na některých předpokladech o datech (většinou můj odhad na schématu) a vyžaduje odbornou implementaci, zejména pokud jde o vhodné indexy pro podporu plánů údržby indexovaných pohledů. sdílím níže uvedený kód pro zájem, nenavrhuji, abyste jej implementovali bez <em> velmi opatrní</em> analýzy a testování.</p><pre><code>-- indexed view to optimize the main view create view dbo.v1 with schemabinding as select it.id, it.product\_code\_v42, it.trans\_type\_v41, it.booking\_no\_v32, it.trans\_qty, it.qtyreturned, it.qtycheckedout, it.qtyreserved, it.bit\_field\_v41, it.prep\_on, it.from\_locn, it.trans\_to\_locn, it.pdate, it.firstdate, it.ptimeh, it.ptimem, it.retndate, it.bookdate, it.timebookedh, it.timebookedm, it.timebookeds, it.del\_time\_hour, it.del\_time\_min, it.return\_to\_locn, it.return\_time\_hour, it.return\_time\_min, it.assignto, it.assigntype, it.inrack from dbo.tblitemtran as it join dbo.tblbookings as tb on tb.booking\_no = it.booking\_no\_v32 where ( it.trans\_type\_v41 not in (2, 3, 7, 18, 19, 20, 21, 12, 13, 22) and it.trans\_type\_v41 not in (6, 7) and it.bit\_field\_v41 &amp; convert(tinyint, 4) = 0 ) or ( it.trans\_type\_v41 not in (6, 7) and it.bit\_field\_v41 &amp; convert(tinyint, 4) = 0 and tb.bookingprogressstatus = 1 ) or ( it.trans\_type\_v41 in (6, 7) and it.bit\_field\_v41 &amp; convert(tinyint, 4) = 0 and it.qtycheckedout = 0 ) or ( it.trans\_type\_v41 in (6, 7) and it.bit\_field\_v41 &amp; convert(tinyint, 4) = 0 and it.qtycheckedout &gt; 0 and it.trans\_qty - (it.qtycheckedout - it.qtyreturned) &gt; 0 ); go create unique clustered index cuq on dbo.v1 (product\_code\_v42, id); go </code></pre><p>existující pohled se vyladil, aby používal indexované zobrazení výše:</p><pre><code>create view [dbo].[vwreallyslowview2] as select i.booking\_no\_v32 as bkno, i.trans\_type\_v41 as trantype, b.assigned\_to\_v61 as assignbk, b.order\_date as dateo, b.hourbooked as hbooked, b.minbooked as mbooked, b.secbooked as sbooked, i.prep\_on as pon, i.from\_locn as flocn, i.trans\_to\_locn as ttlocn, case i.prep\_on when 'y' then i.pdate else i.firstdate end as prdate, i.ptimeh as prtimeh, i.ptimem as prtimem, case when i.retndate &lt; i.firstdate then i.firstdate else i.retndate end as rdatev, i.bit\_field\_v41 as bitfield, i.firstdate as fdatev, i.bookdate as dbooked, i.timebookedh as tbookh, i.timebookedm as tbookm, i.timebookeds as tbooks, i.del\_time\_hour as dth, i.del\_time\_min as dtm, i.return\_to\_locn as rtlocn, i.return\_time\_hour as rth, i.return\_time\_min as rtm, case when i.trans\_type\_v41 in (6, 7) and i.trans\_qty &lt; i.qtycheckedout then 0 when i.trans\_type\_v41 in (6, 7) and i.trans\_qty &gt;= i.qtycheckedout then i.trans\_qty - i.qtycheckedout else i.trans\_qty end as trqty, case when i.trans\_type\_v41 in (6, 7) then 0 else i.qtycheckedout end as myqtycheckedout, case when i.trans\_type\_v41 in (6, 7) then 0 else i.qtyreturned end as retqty, i.id, b.bookingprogressstatus as bkprog, i.product\_code\_v42, i.return\_to\_locn, i.assignto, i.assigntype, i.qtyreserved, b.deprepon, case b.deprepon when 1 then b.deprepdatetime else i.retndate end as deprepdatetime, i.inrack from dbo.v1 as i with (noexpand) join dbo.tblbookings as b on b.booking\_no = i.booking\_no\_v32 join dbo.tblinvmas as m on i.product\_code\_v42 = m.product\_code; </code></pre><p>příklad dotazu a plán provedení:</p><pre><code>select vrsv.* from dbo.vwreallyslowview2 as vrsv where vrsv.product\_code\_v42 = 'm10bolt'; </code></pre><p><img src="https://i.stack.imgur.com/vg5cz.jpg" alt="new execution plan"></p><p>v novém plánu má hash shoda <em> žádný zbytkový predikát</em>, existuje <em> žádný složitý filtr</em>, <em> žádný zbytkový predikát</em> zapnuto hledat indexované zobrazení a <em> odhady kardinality</em> jsou přesně správné.</p><p>jako příklad toho, jak by byly ovlivněny plány vložení/aktualizace/odstranění, je to plán vložení do tabulky itemtrans:</p><p><img src="https://i.stack.imgur.com/kffk5.jpg" alt="insert plan"></p><p>zvýrazněná část je nová a vyžaduje se pro údržbu indexovaného pohledu. cívka tabulky nahradí vložené řádky základních tabulek pro údržbu indexovaného pohledu. každý řádek je spojen s tabulkou rezervací pomocí hledání seskupeného indexu, poté filtr použije složité predikáty klauzule <code>where</code>, aby se zjistilo, zda je třeba řádek přidat do pohledu. pokud ano, provede se vložení do seskupeného indexu pohledu.</p><p>stejný test <code>select * from view</code> provedený dříve byl dokončen za 150 ms s indexovaným pohledem na místě.</p><p>poslední věc: všiml jsem si, že váš server 2008 r2 je stále v rtm. neopraví to vaše problémy s výkonem, ale <a href="https://www.microsoft.com/en-us/download/details.aspx?id=30437">service pack 2 pro 2008 r2</a> je k dispozici od července 2012 a existuje mnoho dobrých důvodů, jak udržet co nejaktuálnější s aktualizacemi service pack.</p> <p>příkaz <code>merge</code> má složitou syntaxi a ještě složitější implementaci, ale v zásadě jde o spojení dvou tabulek, filtrování dolů na řádky, které je třeba změnit (vložit, aktualizovat nebo smazat), a poté provést požadované změny. vzhledem k těmto vzorovým údajům:</p><pre><code>declare @categoryitem as table ( categoryid integer not null, itemid integer not null, primary key (categoryid, itemid), unique (itemid, categoryid) ); declare @datasource as table ( categoryid integer not null, itemid integer not null primary key (categoryid, itemid) ); insert @categoryitem (categoryid, itemid) values (1, 1), (1, 2), (1, 3), (2, 1), (2, 3), (3, 5), (3, 6), (4, 5); insert @datasource (categoryid, itemid) values (2, 2); </code></pre><p><strong> cíl </strong></p><pre><code>╔════════════╦════════╗ ║ categoryid ║ itemid ║ ╠════════════╬════════╣ ║ 1 ║ 1 ║ ║ 2 ║ 1 ║ ║ 1 ║ 2 ║ ║ 1 ║ 3 ║ ║ 2 ║ 3 ║ ║ 3 ║ 5 ║ ║ 4 ║ 5 ║ ║ 3 ║ 6 ║ ╚════════════╩════════╝ </code></pre><p><strong> zdroj </strong></p><pre><code>╔════════════╦════════╗ ║ categoryid ║ itemid ║ ╠════════════╬════════╣ ║ 2 ║ 2 ║ ╚════════════╩════════╝ </code></pre><p>požadovaným výsledkem je nahrazení dat v cíli daty ze zdroje, ale pouze pro <code>categoryid = 2</code>. po výše uvedeném popisu <code>merge</code> bychom měli napsat dotaz, který spojuje zdroj a cíl pouze na klíče, a filtrovat řádky pouze v klauzulích <code>when</code>:</p><pre><code>merge into @categoryitem as target using @datasource as source on source.itemid = target.itemid and source.categoryid = target.categoryid when not matched by source and target.categoryid = 2 then delete when not matched by target and source.categoryid = 2 then insert (categoryid, itemid) values (categoryid, itemid) output $action, isnull(inserted.categoryid, deleted.categoryid) as categoryid, isnull(inserted.itemid, deleted.itemid) as itemid ; </code></pre><p>to dává následující výsledky:</p><pre><code>╔═════════╦════════════╦════════╗ ║ $action ║ categoryid ║ itemid ║ ╠═════════╬════════════╬════════╣ ║ delete ║ 2 ║ 1 ║ ║ insert ║ 2 ║ 2 ║ ║ delete ║ 2 ║ 3 ║ ╚═════════╩════════════╩════════╝ ╔════════════╦════════╗ ║ categoryid ║ itemid ║ ╠════════════╬════════╣ ║ 1 ║ 1 ║ ║ 1 ║ 2 ║ ║ 1 ║ 3 ║ ║ 2 ║ 2 ║ ║ 3 ║ 5 ║ ║ 3 ║ 6 ║ ║ 4 ║ 5 ║ ╚════════════╩════════╝ </code></pre><p>realizační plán je: <img src="https://i.stack.imgur.com/fovhu.jpg" alt="merge plan"></p><p>všimněte si, že jsou obě tabulky plně naskenovány. můžeme to považovat za neefektivní, protože pouze řádky, kde <code>categoryid = 2</code> bude ovlivněno v cílové tabulce. zde přicházejí varování v books online. jeden chybný pokus o optimalizaci, aby se dotkl pouze nezbytných řádků v cíli, je:</p><pre><code>merge into @categoryitem as target using ( select categoryid, itemid from @datasource as ds where categoryid = 2 ) as source on source.itemid = target.itemid and target.categoryid = 2 when not matched by target then insert (categoryid, itemid) values (categoryid, itemid) when not matched by source then delete output $action, isnull(inserted.categoryid, deleted.categoryid) as categoryid, isnull(inserted.itemid, deleted.itemid) as itemid ; </code></pre><p>logika v klauzuli <code>on</code> je použita jako součást spojení. v tomto případě je spojení úplným vnějším spojením (viz <a href="http://technet.microsoft.com/en-us/library/bb522522%28v=sql.105%29.aspx">tento záznam books online</a> proč). použití kontroly kategorie 2 na cílových řádcích jako součásti vnějšího spojení nakonec povede k odstranění řádků s jinou hodnotou (protože neodpovídají zdroji):</p><pre><code>╔═════════╦════════════╦════════╗ ║ $action ║ categoryid ║ itemid ║ ╠═════════╬════════════╬════════╣ ║ delete ║ 1 ║ 1 ║ ║ delete ║ 1 ║ 2 ║ ║ delete ║ 1 ║ 3 ║ ║ delete ║ 2 ║ 1 ║ ║ insert ║ 2 ║ 2 ║ ║ delete ║ 2 ║ 3 ║ ║ delete ║ 3 ║ 5 ║ ║ delete ║ 3 ║ 6 ║ ║ delete ║ 4 ║ 5 ║ ╚═════════╩════════════╩════════╝ ╔════════════╦════════╗ ║ categoryid ║ itemid ║ ╠════════════╬════════╣ ║ 2 ║ 2 ║ ╚════════════╩════════╝ </code></pre><p>kořenová příčina je ze stejného důvodu, proč se predikáty chovají v klauzuli vnější spojení <code>on</code> odlišně, než když jsou specifikovány v klauzuli <code>where</code>. syntaxe <code>merge</code> (a implementace spojení v závislosti na zadaných klauzulích) jen ztěžují vidět, že tomu tak je.</p><p> <a href="http://technet.microsoft.com/en-us/library/bb510625%28v=sql.105%29.aspx">navádění v books online</a> (rozšířeno v položce <a href="http://technet.microsoft.com/en-us/library/cc879317%28v=sql.105%29.aspx">optimizing performance</a> ) nabízí navádění, které zajistí, že správná sémantika je vyjádřena pomocí syntaxe <code>merge</code>, aniž by uživatel nutně musel muset pochopit všechny podrobnosti implementace nebo zohlednit způsoby, kterými by optimalizátor mohl legitimně přeskupit věci z důvodů efektivnosti provádění.</p><p>dokumentace nabízí tři možné způsoby implementace časného filtrování:</p><p><strong> zadání podmínky filtrování v klauzuli <code>when</code> </strong> zaručuje správné výsledky, ale může znamenat, že více řádků je čteno a zpracováno ze zdroje a cíle tabulky, než je nezbytně nutné (viz první příklad).</p><p><strong> aktualizace prostřednictvím zobrazení </strong>, která obsahuje podmínku filtrování, také zaručuje správné výsledky (protože změněné řádky musí být přístupné pro aktualizaci prostřednictvím zobrazení), ale vyžaduje to vyhrazené zobrazení, které splňuje liché podmínky pro aktualizaci zobrazení.</p><p><strong> použití společného výrazu tabulky </strong> přináší podobné riziko při přidávání predikátů k klauzuli <code>on</code>, ale z poněkud odlišných důvodů. v mnoha případech to bude bezpečné, ale vyžaduje to odbornou analýzu prováděcího plánu, aby se to potvrdilo (a rozsáhlé praktické testování). například:</p><pre><code>with target as ( select * from @categoryitem where categoryid = 2 ) merge into target using ( select categoryid, itemid from @datasource where categoryid = 2 ) as source on source.itemid = target.itemid and source.categoryid = target.categoryid when not matched by target then insert (categoryid, itemid) values (categoryid, itemid) when not matched by source then delete output $action, isnull(inserted.categoryid, deleted.categoryid) as categoryid, isnull(inserted.itemid, deleted.itemid) as itemid ; </code></pre><p>výsledkem jsou správné výsledky (neopakované) s optimálnějším plánem:</p><p><img src="https://i.stack.imgur.com/tazmk.jpg" alt="merge plan 2"></p><p>plán čte pouze řádky pro kategorii 2 z cílové tabulky. to může být důležitým hlediskem výkonu, pokud je cílová tabulka velká, ale je příliš snadné tuto chybu získat pomocí syntaxe <code>merge</code>.</p><p>někdy je snazší zapsat <code>merge</code> jako samostatné operace dml. tento přístup může dokonce <strong> vést lépe </strong> než jediný <code>merge</code>, což je skutečnost, která lidi často překvapuje.</p><pre><code>delete ci from @categoryitem as ci where ci.categoryid = 2 and not exists ( select 1 from @datasource as ds where ds.itemid = ci.itemid and ds.categoryid = ci.categoryid ); insert @categoryitem select ds.categoryid, ds.itemid from @datasource as ds where ds.categoryid = 2; </code></pre> <h2>souhrn</h2><p>nechte propojený server dělat co nejvíce.<br> je <em> nemožné </em> pro sql server optimalizovat dotaz na propojeném serveru, dokonce i na jiném serveru sql</p><h2>dlouho</h2><p>klíčovým faktorem je <em> kde </em> je dotaz spuštěn.</p><p>v tomto případě se jedná o triviální výběr, takže všechny řádky z tabulky budou odeslány po drátu. nezáleží na tom.</p><p>když přidáte připojení a kde, pak na tom může záležet. chcete, aby sql server umožnil propojenému serveru provést co největší filtrování, aby se zmenšila velikost dat přicházejících po síti.</p><p>například druhý případ by zde měl být efektivnější.</p><pre><code>select * from openquery(&lt;linked server&gt;, 'select &lt;column list&gt; from mytable') t1 join somelocaltable t2 on ... where t1.foo = 'bar' select * from openquery(&lt;linked server&gt;, 'select &lt;column list&gt; from mytable where foo = ''bar''') join somelocaltable t2 on ... </code></pre><p>omezení openquery je v tom, že nemůžete parametrizovat: proto potřebujete dynamický sql, abyste mohli přidat klauzule where atd.</p><p>výkon propojených serverů může být ovlivněn <a href="http://msdn.microsoft.com/en-us/library/ms178532.aspx"><code>sp\_serveroption</code></a> . nastavení <code>collation compatible</code> říká vše</p><blockquote> <p>pokud je tato možnost nastavena na true, sql server předpokládá, že všechny znaky na propojeném serveru jsou kompatibilní s místním serverem, pokud jde o znakovou sadu a pořadí řazení (nebo pořadí řazení). to umožňuje serveru sql server odesílat porovnání sloupců znaků zprostředkovateli. pokud tato možnost není nastavena, sql server vždy vyhodnocuje porovnání na sloupcích znaků místně.</p> </blockquote><p>to znamená, zkuste nenechat sql server zpracovat data místně.</p><p>poznámka: na mé <code>foo = 'bar'</code> druhý příklad výše je filtr odeslán na propojený server, protože je to pouze řetězcová konstanta na sql server. skutečná klauzule where v prvním příkladu může nebo nemusí být zaslána vzdáleně.</p><p>nakonec jsem také zjistil, že rozložení dat do dočasné tabulky a jejich připojení k místním tabulkám je často lepší než připojení přímo k openquery.</p>