StarRocks Champion带你解构 Optimizer 实现全过程
发布时间:2024-07-29 14:30:13 作者:佚名
<p data-pid="OtSYVYAR">厉害了 竟然还有专业这么对口的研究生我们这里还招人欢迎私聊!</p>
<p data-pid="3VsBqgur">过去一年间,对优化器相关论文做了个系统性的学习,把过程中阅读的论文笔记记录在这里,和大家分享,欢迎大家和我一起讨论,纠错补差,共同进步 ~</p><p data-pid="LXi0scgI">阅读路线基本遵照了pingcap github上的一个Awesome Database Learning的资料,这个资料非常棒,包含了一些基本的课程 + 书籍,还按照内核中不同模块的不同方面做了分类,非常系统化,尤其是SQL层面非常详尽,正好符合需求,因此阅读基本也是按其中的paper来,并扩展到一些没有涉及的内容,总体目录如下(优化器部分),由于内容较多,主要挑选其中影响力较大的或者最有参考意义的。</p><ul><li data-pid="CS7nZji7">1979, <a href="https://link.zhihu.com/?target=http%3A//citeseerx.ist.psu.edu/viewdoc/download%3Fdoi%3D10.1.1.71.3735%26rep%3Drep1%26type%3Dpdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">Access Path Selection in a Relational Database Management System</a>, SIGMOD</li></ul><a data-draft-node="block" data-draft-type="link-card" href="https://zhuanlan.zhihu.com/p/364303913" class="internal"><span class="invisible">https://</span><span class="visible">zhuanlan.zhihu.com/p/36</span><span class="invisible">4303913</span><span class="ellipsis"></span></a><ul><li data-pid="NuE-lMjm">1979, <a href="https://link.zhihu.com/?target=http%3A//15721.courses.cs.cmu.edu/spring2016/papers/p239-lehman.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">Query Processing in Main Memory Database Management Systems</a>, VLDB</li><li data-pid="aBBwS5KC">1988, <a href="https://link.zhihu.com/?target=https%3A//people.eecs.berkeley.edu/~brewer/cs262/23-lohman88.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">Grammar-like Functional Rules for Representing Query Optimization Alternatives</a>, SIGMOD</li><li data-pid="w-PtVKTR">1993, <a href="https://link.zhihu.com/?target=https%3A//pdfs.semanticscholar.org/a817/a3e74d1663d9eb35b4baf3161ab16f57df85.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">The Volcano Optimizer Generator- Extensibility and Efficient Search</a>, ICDE</li></ul><a data-draft-node="block" data-draft-type="link-card" href="https://zhuanlan.zhihu.com/p/364619893" class="internal"><span class="invisible">https://</span><span class="visible">zhuanlan.zhihu.com/p/36</span><span class="invisible">4619893</span><span class="ellipsis"></span></a><ul><li data-pid="O51-QB5B">1995, <a href="https://link.zhihu.com/?target=https%3A//pdfs.semanticscholar.org/360e/cdfc79850873162ee4185bed8f334da30031.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">The Cascades Framework for Query Optimization</a>, IEEE Data engineering Bulltin</li></ul><a data-draft-node="block" data-draft-type="link-card" href="https://zhuanlan.zhihu.com/p/365085770" class="internal"><span class="invisible">https://</span><span class="visible">zhuanlan.zhihu.com/p/36</span><span class="invisible">5085770</span><span class="ellipsis"></span></a><ul><li data-pid="Ziy7M3bq">1998, <a href="https://link.zhihu.com/?target=https%3A//web.stanford.edu/class/cs345d-01/rl/chaudhuri98.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">An Overview of Query Optimization in Relational Systems</a>, PODS</li><li data-pid="k7pGWboF">2014, <a href="https://link.zhihu.com/?target=http%3A//15721.courses.cs.cmu.edu/spring2016/papers/p337-soliman.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">Orca: A Modular Query Optimizer Architecture for Big Data</a>, SIGMOD</li></ul><a data-draft-node="block" data-draft-type="link-card" href="https://zhuanlan.zhihu.com/p/365496273" class="internal"><span class="invisible">https://</span><span class="visible">zhuanlan.zhihu.com/p/36</span><span class="invisible">5496273</span><span class="ellipsis"></span></a><ul><li data-pid="_TXhV5TU">2016, <a href="https://link.zhihu.com/?target=http%3A//www.vldb.org/pvldb/vol9/p1401-chen.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">The MemSQL Query Optimizer: A modern optimizer for real-time analytics in a distributed database</a>, VLDB</li></ul><a data-draft-node="block" data-draft-type="link-card" href="https://zhuanlan.zhihu.com/p/365744405" class="internal"><span class="invisible">https://</span><span class="visible">zhuanlan.zhihu.com/p/36</span><span class="invisible">5744405</span><span class="ellipsis"></span></a><ul><li data-pid="WlQJEmow">2012, <a href="https://link.zhihu.com/?target=https%3A//dl.acm.org/doi/abs/10.1145/2304510.2304525" class=" wrap external" target="_blank" rel="nofollow noreferrer">Testing the Accuracy of Query Optimizers</a>, DBTest</li></ul><a data-draft-node="block" data-draft-type="link-card" href="https://zhuanlan.zhihu.com/p/365621518" class="internal"><span class="invisible">https://</span><span class="visible">zhuanlan.zhihu.com/p/36</span><span class="invisible">5621518</span><span class="ellipsis"></span></a><ul><li data-pid="1003Qagb">1994, <a href="https://link.zhihu.com/?target=https%3A//homes.cs.washington.edu/~alon/files/vldb94.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">Query Optimization by Predicate Move-Around</a>, VLDB</li></ul><a data-draft-node="block" data-draft-type="link-card" href="https://zhuanlan.zhihu.com/p/542619719" class="internal"><span class="invisible">https://</span><span class="visible">zhuanlan.zhihu.com/p/54</span><span class="invisible">2619719</span><span class="ellipsis"></span></a><ul><li data-pid="gVbh_7xj">1995, <a href="https://link.zhihu.com/?target=https%3A//dl.acm.org/doi/10.5555/645921.673154" class=" wrap external" target="_blank" rel="nofollow noreferrer">Eager Aggregation and Lazy Aggregation</a>, VLDB</li><li data-pid="MsZQvF8W">2000, <a href="https://link.zhihu.com/?target=https%3A//www.microsoft.com/en-us/research/wp-content/uploads/2016/02/tr-2000-31.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">Parameterized Queries and Nesting Equivalences</a>, Microsoft Research</li><li data-pid="VcvrRRBt">2001, <a href="https://link.zhihu.com/?target=http%3A//citeseerx.ist.psu.edu/viewdoc/download%3Fdoi%3D10.1.1.563.8492%26rep%3Drep1%26type%3Dpdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">Orthogonal Optimization of Subqueries and Aggregation</a>, SIGMOD</li><li data-pid="kOg1qxbK">2003, <a href="https://link.zhihu.com/?target=https%3A//thelackthereof.org/docs/library/cs/database/Zuzarte%2C%2520Calisto%2520et%2520al%3A%2520Winmagic%2520-%2520Subquery%2520Elimination%2520Using%2520Window%2520Aggregation.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">WinMagic : Subquery Elimination Using Window Aggregation</a>, SIGMOD</li></ul><a data-draft-node="block" data-draft-type="link-card" href="https://zhuanlan.zhihu.com/p/372710924" class="internal"><span class="invisible">https://</span><span class="visible">zhuanlan.zhihu.com/p/37</span><span class="invisible">2710924</span><span class="ellipsis"></span></a><ul><li data-pid="uHu4B9aK">2006, <a href="https://link.zhihu.com/?target=https%3A//dl.acm.org/doi/10.5555/1182635.1164215" class=" wrap external" target="_blank" rel="nofollow noreferrer">Cost-based query transformation in Oracle</a>, VLDB</li></ul><a data-draft-node="block" data-draft-type="link-card" href="https://zhuanlan.zhihu.com/p/372430733" class="internal"><span class="invisible">https://</span><span class="visible">zhuanlan.zhihu.com/p/37</span><span class="invisible">2430733</span><span class="ellipsis"></span></a><ul><li data-pid="lZCnGOht">2009, <a href="https://link.zhihu.com/?target=https%3A//www.researchgate.net/publication/220538535_Enhanced_Subquery_Optimizations_in_Oracle" class=" wrap external" target="_blank" rel="nofollow noreferrer">Enhanced subquery optimizations in Oracle</a>, VLDB</li></ul><a data-draft-node="block" data-draft-type="link-card" href="https://zhuanlan.zhihu.com/p/372784778" class="internal"><span class="invisible">https://</span><span class="visible">zhuanlan.zhihu.com/p/37</span><span class="invisible">2784778</span><span class="ellipsis"></span></a><ul><li data-pid="7tzkiaOz">2015, <a href="https://link.zhihu.com/?target=http%3A//www.btw-2015.de/res/proceedings/Hauptband/Wiss/Neumann-Unnesting_Arbitrary_Querie.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">Unnesting Arbitrary Queries</a>, BTW</li><li data-pid="qBhifQzb">2017, <a href="https://link.zhihu.com/?target=https%3A//15721.courses.cs.cmu.edu/spring2018/papers/16-optimizer2/hyperjoins-btw2017.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">The Complete Story of Joins</a>, BTW</li></ul><ul><li data-pid="_alPEZy5">2006, <a href="https://link.zhihu.com/?target=http%3A//www.vldb.org/conf/2006/p930-moerkotte.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">Analysis of Two Existing and One New Dynamic Programming Algorithm for the Generation of Optimal Bushy Join Trees without Cross Products</a>, VLDB</li></ul><a data-draft-node="block" data-draft-type="link-card" href="https://zhuanlan.zhihu.com/p/367490874" class="internal"><span class="invisible">https://</span><span class="visible">zhuanlan.zhihu.com/p/36</span><span class="invisible">7490874</span><span class="ellipsis"></span></a><ul><li data-pid="K1rxP0so">2008, <a href="https://link.zhihu.com/?target=https%3A//15721.courses.cs.cmu.edu/spring2020/papers/20-optimizer2/p539-moerkotte.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">Dynamic Programming Strikes Back</a>, SIGMOD</li></ul><a data-draft-node="block" data-draft-type="link-card" href="https://zhuanlan.zhihu.com/p/369046631" class="internal"><span class="invisible">https://</span><span class="visible">zhuanlan.zhihu.com/p/36</span><span class="invisible">9046631</span><span class="ellipsis"></span></a><ul><li data-pid="nYPUefup">2013, <a href="https://link.zhihu.com/?target=https%3A//dl.acm.org/doi/abs/10.1145/2463676.2465314" class=" wrap external" target="_blank" rel="nofollow noreferrer">On the Correct and Complete Enumeration of the Core Search Space</a>, SIGMOD</li></ul><a data-draft-node="block" data-draft-type="link-card" href="https://zhuanlan.zhihu.com/p/369388811" class="internal"><span class="invisible">https://</span><span class="visible">zhuanlan.zhihu.com/p/36</span><span class="invisible">9388811</span><span class="ellipsis"></span></a><ul><li data-pid="emoOGWSm">2018, <a href="https://link.zhihu.com/?target=https%3A//db.in.tum.de/~radke/papers/hugejoins.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">Adaptive Optimization of Very Large Join Queries</a>, SIGMOD</li></ul><a data-draft-node="block" data-draft-type="link-card" href="https://zhuanlan.zhihu.com/p/369267836" class="internal"><span class="invisible">https://</span><span class="visible">zhuanlan.zhihu.com/p/36</span><span class="invisible">9267836</span><span class="ellipsis"></span></a><ul><li data-pid="tcNyvXyk">2018, <a href="https://link.zhihu.com/?target=https%3A//www.comp.nus.edu.sg/~chancy/sigmod18-reorder.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">Improving Join Reorderability with Compensation Operators</a>, SIGMOD</li></ul><ul><li data-pid="-1C7Flk2">1996, <a href="https://link.zhihu.com/?target=https%3A//cs.uwaterloo.ca/~gweddell/cs798/p57-simmen.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">Fundamental Techniques for Order Optimization</a>, SIGMOD</li></ul><a data-draft-node="block" data-draft-type="link-card" href="https://zhuanlan.zhihu.com/p/369771981" class="internal"><span class="invisible">https://</span><span class="visible">zhuanlan.zhihu.com/p/36</span><span class="invisible">9771981</span><span class="ellipsis"></span></a><ul><li data-pid="dc6Ibkt2">2004, <a href="https://link.zhihu.com/?target=https%3A//www.researchgate.net/publication/4084912_An_efficient_framework_for_order_optimization" class=" wrap external" target="_blank" rel="nofollow noreferrer">An Efficient Framework for Order Optimization</a>, ICDE</li><li data-pid="eVEK3cID">2010, <a href="https://link.zhihu.com/?target=http%3A//www.cs.albany.edu/~jhh/courses/readings/zhou10.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">Incorporating Partitioning and Parallel Plans into the SCOPE Optimizer</a>, ICDE</li></ul><a data-draft-node="block" data-draft-type="link-card" href="https://zhuanlan.zhihu.com/p/369898082" class="internal"><span class="invisible">https://</span><span class="visible">zhuanlan.zhihu.com/p/36</span><span class="invisible">9898082</span><span class="ellipsis"></span></a><ul><li data-pid="Y-BH02LF">2011, <a href="https://link.zhihu.com/?target=http%3A//www.vldb.org/pvldb/vol4/p843-moerkotte.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">Accelerating Queries with GroupBy and Join by Group join</a>, VLDB</li></ul><a data-draft-node="block" data-draft-type="link-card" href="https://zhuanlan.zhihu.com/p/370205445" class="internal"><span class="invisible">https://</span><span class="visible">zhuanlan.zhihu.com/p/37</span><span class="invisible">0205445</span><span class="ellipsis"></span></a><ul><li data-pid="zFGCNZm0">1996, <a href="https://link.zhihu.com/?target=https%3A//github.com/pingcap/awesome-database-learning/blob/master" class=" wrap external" target="_blank" rel="nofollow noreferrer">Modelling Costs for a MM-DBMS</a>, in Real-Time Databases</li><li data-pid="U5olvNem">1996, <a href="https://link.zhihu.com/?target=http%3A//web.eecs.umich.edu/~mozafari/fall2015/eecs584/papers/adhoc-joins.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">SEEKing the truth about ad hoc join costs</a>, IBM Almaden Research Center</li><li data-pid="0gtEndMY">2014, <a href="https://link.zhihu.com/?target=https%3A//infoscience.epfl.ch/record/219202/files/p1299-trummer.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">Approximation Schemes for Many-Objective Query Optimization</a>, SIGMOD</li></ul><p data-pid="Oxc34mGa">Histogram</p><ul><li data-pid="fLby-gzl">1984, <a href="https://link.zhihu.com/?target=https%3A//dl.acm.org/doi/pdf/10.1145/971697.602294" class=" wrap external" target="_blank" rel="nofollow noreferrer">Accurate Estimation of the Number of Tuples Satisfying a Condition</a>, SIGMOD</li><li data-pid="_9Z9ZyNr">1993, <a href="https://link.zhihu.com/?target=https%3A//dl.acm.org/doi/pdf/10.1145/169725.169708" class=" wrap external" target="_blank" rel="nofollow noreferrer">Optimal Histograms for Limiting Worst-Case Error Propagation in the Size of Join Results</a>, ACM Trans. on Database Systems</li><li data-pid="C44bMEMr">1993, <a href="https://link.zhihu.com/?target=https%3A//pdfs.semanticscholar.org/deeb/d2fa377a41de49e5556ea948191a741faa1e.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">Universality of Serial Histograms</a>, VLDB</li><li data-pid="OhvNb9sK">1995, <a href="https://link.zhihu.com/?target=https%3A//dl.acm.org/doi/pdf/10.1145/223784.223841" class=" wrap external" target="_blank" rel="nofollow noreferrer">Balancing Histogram Optimality and Practicality for Query Result Size Estimation</a>, SIGMOD</li><li data-pid="_DHDzIVO">1996, <a href="https://link.zhihu.com/?target=https%3A//dl.acm.org/doi/pdf/10.1145/233269.233342" class=" wrap external" target="_blank" rel="nofollow noreferrer">Improved Histograms for Selectivity Estimation of Range Predicates</a>, SIGMOD</li><li data-pid="tC0Lr099">1997, <a href="https://link.zhihu.com/?target=https%3A//dl.acm.org/doi/pdf/10.1007/s007780050043%3Fdownload%3Dtrue" class=" wrap external" target="_blank" rel="nofollow noreferrer">SEEKing the truth about ad hoc join costs</a>, VLDB</li><li data-pid="Qp2ool0R">2003, <a href="https://link.zhihu.com/?target=http%3A//www.madgik.di.uoa.gr/sites/default/files/vldb03_pp19-30.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">The History of Histograms</a>, VLDB</li><li data-pid="TKbfUf6k">2009, <a href="https://link.zhihu.com/?target=https%3A//dl.acm.org/doi/pdf/10.14778/1687627.1687738" class=" wrap external" target="_blank" rel="nofollow noreferrer">Preventing Bad Plans by Bounding the Impact of Cardinality Estimation Errors</a>, VLDB</li><li data-pid="10EGmvEt">2010, <a href="https://link.zhihu.com/?target=https%3A//dl.acm.org/doi/pdf/10.1145/1807167.1807239" class=" wrap external" target="_blank" rel="nofollow noreferrer">Histograms Reloaded: The Merits of Bucket Diversity</a>, SIGMOD</li><li data-pid="K8Vn42n1">2014, <a href="https://link.zhihu.com/?target=https%3A//dl.acm.org/doi/pdf/10.1145/2588555.2595629" class=" wrap external" target="_blank" rel="nofollow noreferrer">Exploiting Ordered Dictionaries to Efficiently Construct Histograms with Q-Error Guarantees in SAP HANA</a>, SIGMOD</li><li data-pid="awbY22EW">2015, <a href="https://link.zhihu.com/?target=http%3A//www.vldb.org/pvldb/vol9/p204-leis.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">How Good Are Query Optimizers, Really?</a>, VLDB</li></ul><p data-pid="V-YcnMVi">Probabilistic Counting</p><ul><li data-pid="FY0xqpRM">2000, <a href="https://link.zhihu.com/?target=https%3A//dl.acm.org/doi/pdf/10.1145/335168.335230" class=" wrap external" target="_blank" rel="nofollow noreferrer">Towards Estimation Error Guarantees for Distinct Values</a>, SIGMOD/PODS</li><li data-pid="VgqhGTE6">2001, <a href="https://link.zhihu.com/?target=http%3A//vldb.org/conf/2001/P541.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">Distinct Sampling for Highly-Accurate Answers to Distinct Values Queries and Event Reports</a>, VLDB</li><li data-pid="bfMoGrMh">2005, <a href="https://link.zhihu.com/?target=https%3A//dsf.berkeley.edu/cs286/papers/countmin-latin2004.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">An Improved Data Stream Summary:The Count-Min Sketch and its Applications</a> Journal of Algorithms</li><li data-pid="GzAaw4Zm">2007, <a href="https://link.zhihu.com/?target=http%3A//webdocs.cs.ualberta.ca/~drafiei/papers/cmm.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">New Estimation Algorithms for Streaming Data: Count-min Can Do More</a></li><li data-pid="SS5ys3-j">2013, <a href="https://link.zhihu.com/?target=https%3A//research.google/pubs/pub40671/" class=" wrap external" target="_blank" rel="nofollow noreferrer">HyperLogLog in Practice: Algorithmic Engineering of a State of The Art Cardinality Estimation Algorithm</a>, ACM</li><li data-pid="535HX4b-">2019,<a href="https://link.zhihu.com/?target=https%3A//db.in.tum.de/~freitag/papers/p23-freitag-cidr19.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">Every Row Counts: Combining Sketches and Sampling for Accurate Group-By Result Estimates</a>, CIDR</li></ul><a data-draft-node="block" data-draft-type="link-card" href="https://zhuanlan.zhihu.com/p/483848185" class="internal"><span class="invisible">https://</span><span class="visible">zhuanlan.zhihu.com/p/48</span><span class="invisible">3848185</span><span class="ellipsis"></span></a><p data-pid="FJ6YFR64">Others</p><ul><li data-pid="qHozLp2y">2002, <a href="https://link.zhihu.com/?target=http%3A//citeseerx.ist.psu.edu/viewdoc/download%3Fdoi%3D10.1.1.537.7454%26rep%3Drep1%26type%3Dpdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">Exploiting Statistics on Query Expressions for Optimization</a>, ACM</li><li data-pid="v2-GE9kn">2017, <a href="https://link.zhihu.com/?target=http%3A//www.vldb.org/pvldb/vol10/p1813-zait.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">Adaptive Statistics in Oracle 12c</a>, VLDB</li><li data-pid="HqoYKpw5">2017, <a href="https://link.zhihu.com/?target=http%3A//www.vldb.org/pvldb/vol10/p1658-nica.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">Statisticum: Data Statistics Management in SAP HANA</a>, VLDB</li><li data-pid="Mx1Aa6tg">2019, <a href="https://link.zhihu.com/?target=https%3A//dl.acm.org/doi/pdf/10.1145/3299869.3319894" class=" wrap external" target="_blank" rel="nofollow noreferrer">Pessimistic Cardinality Estimation: Tighter Upper Bounds for Intermediate Join Cardinalities</a>, SIGMOD</li><li data-pid="5wBNWa5P">2019, <a href="https://link.zhihu.com/?target=http%3A//www.vldb.org/pvldb/vol13/p279-yang.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">Deep Unsupervised Cardinality Estimation</a>, VLDB</li><li data-pid="aKjHH-aJ">2020, <a href="https://link.zhihu.com/?target=https%3A//vldb.org/pvldb/vol14/p61-yang.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">NeuroCard: One Cardinality Estimator for All Tables</a>, VLDB</li></ul><ul><li data-pid="oyH8ITSe">2001, <a href="https://link.zhihu.com/?target=http%3A//15721.courses.cs.cmu.edu/spring2016/papers/stillger-vldb2001.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">LEO – DB2’s LEarning Optimizer</a>, VLDB</li><li data-pid="pI2mifXs">2004, <a href="https://link.zhihu.com/?target=https%3A//www.cse.iitb.ac.in/infolab/Data/Courses/CS632/2006/Papers/sigmod04-markl.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">Robust Query Processing through Progressive Optimization</a>, SIGMOD</li><li data-pid="EHZQtUuk">2004, <a href="https://link.zhihu.com/?target=http%3A//citeseerx.ist.psu.edu/viewdoc/download%3Fdoi%3D10.1.1.91.4185%26rep%3Drep1%26type%3Dpdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">Automated Statistics Collection in DB2 UDB</a>, VLDB</li><li data-pid="b-6xdVea">2008, <a href="https://link.zhihu.com/?target=https%3A//www.semanticscholar.org/paper/Optimizer-plan-change-management%253A-improved-and-in-Ziauddin-Das/04525b1c5a3e7c6ee33c91a219306ea6378d4d97" class=" wrap external" target="_blank" rel="nofollow noreferrer">Optimizer plan change management: improved stability and performance in Oracle 11g</a>, VLDB</li><li data-pid="9LJf-Sa2">2015, <a href="https://link.zhihu.com/?target=http%3A//cidrdb.org/cidr2005/papers/P20.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">Adaptive Query Processing in the Looking Glass</a>, CIDR</li></ul><ul><li data-pid="JpziWZSY">1995, <a href="https://link.zhihu.com/?target=https%3A//ieeexplore.ieee.org/document/5387270" class=" wrap external" target="_blank" rel="nofollow noreferrer">DB2 Parallel Edition</a>, IBM System Journal</li><li data-pid="JFxsSY6M">2004, <a href="https://link.zhihu.com/?target=https%3A//dl.acm.org/doi/abs/10.1145/1007568.1007666" class=" wrap external" target="_blank" rel="nofollow noreferrer">Parallel SQL execution in Oracle 10g</a>, SIGMOD</li></ul><a data-draft-node="block" data-draft-type="link-card" href="https://zhuanlan.zhihu.com/p/375852049" class="internal"><span class="invisible">https://</span><span class="visible">zhuanlan.zhihu.com/p/37</span><span class="invisible">5852049</span><span class="ellipsis"></span></a><ul><li data-pid="jbDmNdx-">2012, <a href="https://link.zhihu.com/?target=https%3A//dl.acm.org/doi/abs/10.1145/2213836.2213953" class=" wrap external" target="_blank" rel="nofollow noreferrer">Query Optimization in Microsoft SQL Server PDW</a>, ACM</li></ul><a data-draft-node="block" data-draft-type="link-card" href="https://zhuanlan.zhihu.com/p/366434087" class="internal"><span class="invisible">https://</span><span class="visible">zhuanlan.zhihu.com/p/36</span><span class="invisible">6434087</span><span class="ellipsis"></span></a><ul><li data-pid="JD1wiQzB">2013, <a href="https://link.zhihu.com/?target=https%3A//dl.acm.org/doi/10.14778/2536222.2536235" class=" wrap external" target="_blank" rel="nofollow noreferrer">Adaptive and big data scale parallel execution in Oracle</a>, VLDB</li></ul><a data-draft-node="block" data-draft-type="link-card" href="https://zhuanlan.zhihu.com/p/366735936" class="internal"><span class="invisible">https://</span><span class="visible">zhuanlan.zhihu.com/p/36</span><span class="invisible">6735936</span><span class="ellipsis"></span></a><ul><li data-pid="Ru33ovzL">2014, <a href="https://link.zhihu.com/?target=https%3A//www.slideshare.net/emcacademics/optimizing-queriesoverpartitionedtablesinmpp-systems-1" class=" wrap external" target="_blank" rel="nofollow noreferrer">Optimizing Queries over Partitioned Tables in MPP Systems</a>, SIGMOD</li></ul><a data-draft-node="block" data-draft-type="link-card" href="https://zhuanlan.zhihu.com/p/367068030" class="internal"><span class="invisible">https://</span><span class="visible">zhuanlan.zhihu.com/p/36</span><span class="invisible">7068030</span><span class="ellipsis"></span></a><ul><li data-pid="9GkOcYoT">2013, <a href="https://link.zhihu.com/?target=https%3A//homepages.cwi.nl/~boncz/snb-challenge/chokepoints-tpctc.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">TPC-H Analyzed: Hidden Messages and Lessons Learned from an Influential Benchmark</a>, <a href="https://link.zhihu.com/?target=http%3A//tpc.org/" class=" wrap external" target="_blank" rel="nofollow noreferrer">tpc.org</a></li></ul><a data-draft-node="block" data-draft-type="link-card" href="https://zhuanlan.zhihu.com/p/369455226" class="internal"><span class="invisible">https://</span><span class="visible">zhuanlan.zhihu.com/p/36</span><span class="invisible">9455226</span><span class="ellipsis"></span></a><ul><li data-pid="KNVZHPxQ">2020, <a href="https://link.zhihu.com/?target=http%3A//www.vldb.org/pvldb/vol13/p1206-dreseler.pdf" class=" wrap external" target="_blank" rel="nofollow noreferrer">Quantifying TPCH Choke Points and Their Optimizations</a>, VLDB</li></ul><a data-draft-node="block" data-draft-type="link-card" href="https://zhuanlan.zhihu.com/p/369619142" class="internal"><span class="invisible">https://</span><span class="visible">zhuanlan.zhihu.com/p/36</span><span class="invisible">9619142</span><span class="ellipsis"></span></a><p></p>
<p data-pid="s19aEnZq">推荐一些可帮助有效监控数据库和优化 SQL 查询的工具:</p><p data-pid="O1Wafcbc"><b>一、EverSQL</b></p><p data-pid="30n6YRd9"><a href="https://link.zhihu.com/?target=https%3A//www.eversql.com/" class=" external" target="_blank" rel="nofollow noreferrer"><span class="invisible">https://www.</span><span class="visible">eversql.com/</span><span class="invisible"></span></a></p><figure data-size="normal"><img src="https://pica.zhimg.com/v2-bc827c5e2a24d0ccfb80b035f633d42b_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1000" data-rawheight="519" data-original-token="v2-bc827c5e2a24d0ccfb80b035f633d42b" data-default-watermark-src="https://pic1.zhimg.com/50/v2-7f32ff4b0b963eea78360b3a897c3396_720w.jpg?source=1940ef5c" class="origin_image zh-lightbox-thumb" width="1000" data-original="https://pica.zhimg.com/v2-bc827c5e2a24d0ccfb80b035f633d42b_r.jpg?source=1940ef5c"/></figure><p data-pid="h42K7qCT">EverSQL是由 AI 驱动的用于优化SQL 查询和监控数据库的有趣选项之一。可以免费帮助开发人员、DBA和DevOps工程师节省宝贵的时间,上手完全免费。它支持各种数据库(MySQL、PostgreSQL、MariaDB、MongoDB等)、操作系统和云平台。</p><p data-pid="7KbURRds"><b>二、Paessler PRTG 网络监视器</b></p><p data-pid="eirW2HaW"><a href="https://link.zhihu.com/?target=https%3A//www.paessler.com/" class=" external" target="_blank" rel="nofollow noreferrer"><span class="invisible">https://www.</span><span class="visible">paessler.com/</span><span class="invisible"></span></a></p><figure data-size="normal"><img src="https://pic1.zhimg.com/v2-4cbb8540f671a9bd552cc543d41e2f96_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1000" data-rawheight="410" data-original-token="v2-4cbb8540f671a9bd552cc543d41e2f96" data-default-watermark-src="https://picx.zhimg.com/50/v2-f8dfca9b31750501423cfa32fc8f3b75_720w.jpg?source=1940ef5c" class="origin_image zh-lightbox-thumb" width="1000" data-original="https://pic1.zhimg.com/v2-4cbb8540f671a9bd552cc543d41e2f96_r.jpg?source=1940ef5c"/></figure><p data-pid="PeD1NYoV">PRTG Network Monitor 是一项专门定制的服务,可帮助监控数据库。它提供所有数据库的简明概览,以轻松帮助跟踪所有内容。还可以在需要时查看特定数据库的详细报告。虽然它不直接优化 SQL 查询,但洞察力会告诉我们查询的长度、连接时间和执行时间。多亏了这些见解,我们可以继续选择任何其他优化工具,以根据需要轻松优化查询。</p><p data-pid="Grm82zJc"><b>三、dbForge Studio</b></p><p data-pid="NyqnRbDy"><a href="https://link.zhihu.com/?target=https%3A//www.devart.com/dbforge/sql/studio/sql-query-profiler.html" class=" external" target="_blank" rel="nofollow noreferrer"><span class="invisible">https://www.</span><span class="visible">devart.com/dbforge/sql/</span><span class="invisible">studio/sql-query-profiler.html</span><span class="ellipsis"></span></a></p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-7e704b957d884b9928f641e4c1c7067b_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1000" data-rawheight="750" data-original-token="v2-7e704b957d884b9928f641e4c1c7067b" data-default-watermark-src="https://pica.zhimg.com/50/v2-ec7074b322477c8886b20f020489f8c3_720w.jpg?source=1940ef5c" class="origin_image zh-lightbox-thumb" width="1000" data-original="https://picx.zhimg.com/v2-7e704b957d884b9928f641e4c1c7067b_r.jpg?source=1940ef5c"/></figure><p data-pid="hPxhdx-J">dbForge Studio 是一个成熟的工具包,可帮助我们进行开发和数据库管理。我们可以获得 SQL 编码帮助、添加源代码控制系统、管理索引等等。借助其SQL 查询计划工具,它还可以帮助定位瓶颈并分析查询。在对 SQL 查询进行更改后比较历史数据时,我们可以获得高级统计信息以检测潜在瓶颈。</p><p data-pid="s6A6eiU2">此外,我们还可以获得大量高级工具来更深入地挖掘并有效地管理数据库的各个方面。可以免费试用,但功能有限。如果是专业人士或企业,可以选择高级计划来访问所有可用的高级功能。</p><p data-pid="NTXBGzhZ"><b>四、Plan Explorer</b></p><p data-pid="Q8QskJCh"><a href="https://link.zhihu.com/?target=https%3A//www.sentryone.com/plan-explorer" class=" external" target="_blank" rel="nofollow noreferrer"><span class="invisible">https://www.</span><span class="visible">sentryone.com/plan-expl</span><span class="invisible">orer</span><span class="ellipsis"></span></a></p><figure data-size="normal"><img src="https://pica.zhimg.com/v2-8f7dda42f438327964d50f712dd2843d_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1000" data-rawheight="625" data-original-token="v2-8f7dda42f438327964d50f712dd2843d" data-default-watermark-src="https://pic1.zhimg.com/50/v2-2b2fc32a58525ba448faf9182b23efab_720w.jpg?source=1940ef5c" class="origin_image zh-lightbox-thumb" width="1000" data-original="https://pica.zhimg.com/v2-8f7dda42f438327964d50f712dd2843d_r.jpg?source=1940ef5c"/></figure><p data-pid="FDXh3nuM">Plan Explorer是 Solarwinds 的SentryOne 产品的一部分。它是完全免费的,没有高级升级。</p><p data-pid="Y8bQ1pIv">如果你正在寻找具有高级功能的 SQL 调优工具,这应该是一个不错的选择。该软件工具仅适用于Windows。因此,我们需要安装 .NET 4.7.1 才能在系统上使用它。DBA 和程序员使用 Plan Explorer 的高级查询调整功能,包括历史记录、评论等。</p><p data-pid="H6KLkyof"><b>五、Holistic.dev</b></p><p data-pid="S5jGZKIw"><a href="https://link.zhihu.com/?target=https%3A//holistic.dev/" class=" external" target="_blank" rel="nofollow noreferrer"><span class="invisible">https://</span><span class="visible">holistic.dev/</span><span class="invisible"></span></a></p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-48453b5d43ea754e4dc7e700b93485f3_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="752" data-rawheight="628" data-original-token="v2-48453b5d43ea754e4dc7e700b93485f3" data-default-watermark-src="https://picx.zhimg.com/50/v2-524516a16b55b3ad8dbd8611deac9df8_720w.jpg?source=1940ef5c" class="origin_image zh-lightbox-thumb" width="752" data-original="https://picx.zhimg.com/v2-48453b5d43ea754e4dc7e700b93485f3_r.jpg?source=1940ef5c"/></figure><p data-pid="1VWwzLpg">Holistic.dev是一个有趣的数据库性能优化工具。与 PlanExplorer 不同,不需要安装任何客户端,就可以自动检查数据库日志、分析源代码并利用洞察力提高数据库性能。</p><p data-pid="W8QQRmG1">它还支持自动审计外部数据库。它不支持所有类型的数据库,并且仅限于PostgreSQL。因此,如果它满足我们的要求,可以对其进行试验。</p><p data-pid="SanlhLOR"><b>六、Database Performance Analyzer</b></p><p data-pid="kcX_J8f5"><a href="https://link.zhihu.com/?target=https%3A//www.solarwinds.com/database-performance-analyzer" class=" external" target="_blank" rel="nofollow noreferrer"><span class="invisible">https://www.</span><span class="visible">solarwinds.com/database</span><span class="invisible">-performance-analyzer</span><span class="ellipsis"></span></a></p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-7704748fbd60a90a99f7ca10b0a50935_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1000" data-rawheight="625" data-original-token="v2-7704748fbd60a90a99f7ca10b0a50935" data-default-watermark-src="https://picx.zhimg.com/50/v2-b0565be286223baff2025a6f3d2f5073_720w.jpg?source=1940ef5c" class="origin_image zh-lightbox-thumb" width="1000" data-original="https://picx.zhimg.com/v2-7704748fbd60a90a99f7ca10b0a50935_r.jpg?source=1940ef5c"/></figure><p data-pid="QG0lh_2n">如果你正在寻找适用于云和本地的跨平台解决方案,Database Performance Analyzer将是一个不错的选择。</p><p data-pid="a0gFp-_V">它使用机器学习来检测数据库中的任何异常。除此之外,还可以获得查询调优顾问(响应时间分析、检测性能不佳的语句等)来帮助优化 SQL 查询和数据库。我们可以获得实时和历史数据来分析有关数据库的所有内容。</p><p data-pid="q2Ivmwq-"><b>七、ApexSQL</b></p><p data-pid="qYABZmMm"><a href="https://link.zhihu.com/?target=https%3A//www.apexsql.com/sql-tools-plan.aspx" class=" external" target="_blank" rel="nofollow noreferrer"><span class="invisible">https://www.</span><span class="visible">apexsql.com/sql-tools-p</span><span class="invisible">lan.aspx</span><span class="ellipsis"></span></a></p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-79bc06593180bbab785aa7bc292cf998_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="840" data-rawheight="368" data-original-token="v2-79bc06593180bbab785aa7bc292cf998" data-default-watermark-src="https://pic1.zhimg.com/50/v2-a4370ee7c0fc8916a478314441abf2d3_720w.jpg?source=1940ef5c" class="origin_image zh-lightbox-thumb" width="840" data-original="https://picx.zhimg.com/v2-79bc06593180bbab785aa7bc292cf998_r.jpg?source=1940ef5c"/></figure><p data-pid="UMg4hXnx">ApexSQL可帮助查看和分析 SQL Server 中的执行计划以及更高级的操作。</p><p data-pid="FLoqztg4">我们可以通过查询识别性能问题、了解性能问题、监控实时性能统计等。它作为整个工具包的一部分提供。我们可以根据自己的要求选择购买订阅以获取所有或部分功能。如果需要一些选定的功能,它可以证明是一种经济实惠的商业选择。</p>
<p data-pid="NxOKhCr3">个人理解,学习优化器最好是结合论文和源码,优化器是算法密集型的,不看论文,不知道源码在干什么,整体架构以及模块之间的关系。优化器论文中很多内容晦涩难懂,不看源码,不知道是什么,不知道怎么落地的。</p><p data-pid="h6ZmLFqh">优化器框架:System-R Style框架</p><p data-pid="CesbJ0wh"><u><a href="https://link.zhihu.com/?target=https%3A//developer.aliyun.com/article/949712" class=" wrap external" target="_blank" rel="nofollow noreferrer">OceanBase查询优化器</a></u></p><p data-pid="DyjMU33j"><u><a href="https://link.zhihu.com/?target=https%3A//developer.aliyun.com/article/1089727" class=" wrap external" target="_blank" rel="nofollow noreferrer">OceanBase 查询优化 | 学习笔记</a></u></p><p data-pid="XtF1diDa"><u><a href="https://zhuanlan.zhihu.com/p/37979505" class="internal">来不及解释了!看OceanBase SQL团队如何优雅处理4200万条/秒SQL峰值</a></u></p><p data-pid="6gkY77I_">源码解读</p><p data-pid="PUZBC1cS">竹翁专栏:<u><a href="https://www.zhihu.com/column/c_1386628099518402560" class="internal">开源数据库OceanBase</a></u></p><p data-pid="z3iN42W1"><u><a href="https://zhuanlan.zhihu.com/p/397080656" class="internal">OceanBase源码解读(三):SQL的一生</a></u></p><p data-pid="U-6av4_2"><u><a href="https://zhuanlan.zhihu.com/p/509648894" class="internal">OceanBase源码解读(八):OB高性能执行引擎</a></u></p><p data-pid="bLywDbCd"><u><a href="https://zhuanlan.zhihu.com/p/509650503" class="internal">OceanBase 源码解读(十一):表达式和函数</a></u></p><p data-pid="NoNjumeM">飞驰人生专栏:<u><a href="https://www.zhihu.com/column/c_1601908064378204160" class="internal">Oceanbase查询改写</a></u></p><p data-pid="k1z05RGN">优化器框架:Volcano/Cascades框架</p><p data-pid="XomB_FSG"><u><a href="https://zhuanlan.zhihu.com/p/353161383" class="internal">PolarDB-X 面向 HTAP 的 CBO 优化器</a></u></p><p data-pid="9zZKTcS8"><u><a href="https://zhuanlan.zhihu.com/p/391159830" class="internal">论文解读:机器学习加持的查询优化器(SIGMOD 2021 Best Paper)</a></u></p><p data-pid="90hca2a5"><u><a href="https://zhuanlan.zhihu.com/p/470139328" class="internal">PolarDB-X 优化器核心技术 ~ Join Reorder</a></u></p><p data-pid="_4LIfLaA"><u><a href="https://zhuanlan.zhihu.com/p/354754979" class="internal">查询性能优化之 Runtime Filter</a></u></p><p data-pid="Pq7YT2HA"><u><a href="https://www.zhihu.com/column/c_1457040311961157632" class="internal">PolarDB-X 实现原理</a></u></p><p data-pid="arK25XsJ"><u><a href="https://zhuanlan.zhihu.com/p/370372242" class="internal">PolarDB-X CBO 优化器技术内幕</a></u></p><p data-pid="dDiQFsbf">优化器框架:System-R Style框架</p><p data-pid="cSBowrwd"><u><a href="https://www.zhihu.com/column/c_1531246258613690368" class="internal">PolarDB-MySQL 计算引擎技术</a></u></p><p data-pid="ZNzUTpkq"><u><a href="https://link.zhihu.com/?target=https%3A//help.aliyun.com/document_detail/412392.html" class=" wrap external" target="_blank" rel="nofollow noreferrer">查询优化(Query Optimizer)</a></u></p><p data-pid="yGKcAR66">优化器框架:Volcano/Cascades框架</p><p data-pid="GGYIHhwe"><a href="https://link.zhihu.com/?target=https%3A//help.aliyun.com/document_detail/470242.html%3Fspm%3Da2c4g.11186623.0.0.3ee86596KwfUtG%23%25E5%2588%2586%25E5%25B8%2583%25E5%25BC%258F%25E4%25BC%2598%25E5%258C%2596%25E5%2599%25A8" class=" wrap external" target="_blank" rel="nofollow noreferrer">PolarDB PostgreSQL引擎架构介绍</a></p><p data-pid="xGV5mpiJ">优化器框架:System-R Style框架</p><p data-pid="jBYkU0N3">《数据库查询优化器的艺术:原理解析与 SQL 性能优化》</p><p data-pid="JeuPXyaW"><u><a href="https://www.zhihu.com/people/yaoling-lc" class="internal">henry liang-lc</a></u> <u><a href="https://www.zhihu.com/column/c_1453135878944567296" class="internal">MySQL 源码解析专栏</a></u></p><p data-pid="ND92U3Fq"><u><a href="https://link.zhihu.com/?target=http%3A//blog.chinaunix.net/uid-26896862-id-3218584.html" class=" wrap external" target="_blank" rel="nofollow noreferrer">MySQL查询优化器源码分析</a></u></p><p data-pid="cBLllAh5"><u><a href="https://link.zhihu.com/?target=https%3A//developer.aliyun.com/article/789478%3Fspm%3Da2c6h.13262185.profile.77.708758bbNRK1g9" class=" wrap external" target="_blank" rel="nofollow noreferrer">MySQL · Optimizer · Optimizer Hints</a></u></p><p data-pid="geDXdDKg">优化器框架:System-R Style框架</p><p data-pid="f6-2J9kN"><u><a href="https://link.zhihu.com/?target=https%3A//dbaplus.cn/news-155-2060-1.html" class=" wrap external" target="_blank" rel="nofollow noreferrer">PostgreSQL查询优化器详解(逻辑优化篇)</a></u></p><p data-pid="mBNudHyh"><u><a href="https://zhuanlan.zhihu.com/p/56702915" class="internal">PostgreSQL 优化器代码概览</a></u></p><p data-pid="6Uk8nGa-"><u><a href="https://zhuanlan.zhihu.com/p/583108869" class="internal">PostgreSQL JIT(Just-In-Time Compilation)With LLVM 的实现原理</a></u></p><p data-pid="OwEC4P5H"><u><a href="https://link.zhihu.com/?target=https%3A//bbs.huaweicloud.com/blogs/349152" class=" wrap external" target="_blank" rel="nofollow noreferrer">PostgreSQL代价模型</a></u></p><p data-pid="XCi8ho0D">《数据库查询优化器的艺术:原理解析与 SQL 性能优化》</p><p data-pid="vdH-BFvz">《PostgreSQL数据库内核分析》</p><p data-pid="NpYmXjQV">《PostgreSQL技术内幕:查询优化深度探索》</p><p data-pid="VSST7I6r">优化器框架:Volcano/Cascades框架</p><p data-pid="be6tphO7"><u><a href="https://zhuanlan.zhihu.com/p/476320973" class="internal">ORCA 源码阅读 Overview</a></u></p><p data-pid="tbduiQhg">优化器框架:Volcano/Cascades框架</p><p data-pid="L5C6AfJI"><u><a href="https://zhuanlan.zhihu.com/p/578751676" class="internal">TiDB 优化器之Join Reorder小结</a></u></p><p data-pid="_O7i52e7"><u><a href="https://zhuanlan.zhihu.com/p/94079481" class="internal">揭秘 TiDB 新优化器:Cascades Planner 原理解析</a></u></p><p data-pid="6mq0SPQp"><u><a href="https://zhuanlan.zhihu.com/p/195112860" class="internal">使用 Horoscope 测试 TiDB 优化器</a></u></p><p data-pid="ie9ZD6Xf"><u><a href="https://zhuanlan.zhihu.com/p/498292692" class="internal">TiDB 查询优化及调优系列(一)TiDB 优化器简介</a></u></p><p data-pid="5wV4KAkg"><u><a href="https://zhuanlan.zhihu.com/p/505810001" class="internal">TiDB 查询优化及调优系列(二)TiDB 查询计划简介</a></u></p><p data-pid="78V09hX2"><u><a href="https://zhuanlan.zhihu.com/p/35511864" class="internal">TiDB 源码阅读系列文章(七)基于规则的优化</a></u></p><p data-pid="cxtIYATX"><u><a href="https://zhuanlan.zhihu.com/p/520372189" class="internal">TiDB 查询优化及调优系列(四)查询执行计划的调整及优化原理</a></u></p><p data-pid="XWKEUntw"><u><a href="https://zhuanlan.zhihu.com/p/36420449" class="internal">TiDB 源码阅读系列文章(八)基于代价的优化</a></u></p><p data-pid="C4KGT5id"><u><a href="https://zhuanlan.zhihu.com/p/60931851" class="internal">TiDB 源码学习:常见子查询优化</a></u></p><p data-pid="0hmBOypL"><u><a href="https://zhuanlan.zhihu.com/p/504114469" class="internal">深入了解 TiDB SQL 优化器</a></u></p><p data-pid="bSxbuQmT"><a href="https://link.zhihu.com/?target=https%3A//cn.pingcap.com/blog/10mins-become-contributor-20191126/" class=" wrap external" target="_blank" rel="nofollow noreferrer">十分钟成为 Contributor 系列 | 为 Cascades Planner 添加优化规则 | PingCAP</a></p><p data-pid="DSsCWVgi"><u><a href="https://zhuanlan.zhihu.com/p/37773956" class="internal">TiDB 源码阅读系列文章(九)Hash Join</a></u></p><p data-pid="-aH4WnGi"><a href="https://zhuanlan.zhihu.com/p/40079139" class="internal">TiDB Robot:TiDB 源码阅读系列文章(十四)统计信息(下)</a></p><p data-pid="rlAFroM2"><a href="https://zhuanlan.zhihu.com/p/39139693" class="internal">TiDB Robot:TiDB 源码阅读系列文章(十二)统计信息(上)</a></p><p data-pid="o65Jojdh"><a href="https://zhuanlan.zhihu.com/p/52969666" class="internal">TiDB Robot:TiDB 源码阅读系列文章(二十二)Hash Aggregation</a></p><p data-pid="EZ8wp25Q"><a href="https://zhuanlan.zhihu.com/p/39649659" class="internal">TiDB Robot:TiDB 源码阅读系列文章(十三)索引范围计算简介</a></p><p data-pid="J1WyhzQk"><a href="https://zhuanlan.zhihu.com/p/38572730" class="internal">TiDB Robot:TiDB 源码阅读系列文章(十一)Index Lookup Join</a></p><p data-pid="3F1E5RMN"><a href="https://zhuanlan.zhihu.com/p/35511864" class="internal">TiDB Robot:TiDB 源码阅读系列文章(七)基于规则的优化</a></p><p data-pid="_AIosAcF"><a href="https://zhuanlan.zhihu.com/p/36420449" class="internal">TiDB Robot:TiDB 源码阅读系列文章(八)基于代价的优化</a></p><p data-pid="WcH4TLBM"><a href="https://zhuanlan.zhihu.com/p/34369624" class="internal">TiDB Robot:TiDB 源码阅读系列文章(三)SQL 的一生</a></p><p data-pid="EGcYA0Xt"><a href="https://zhuanlan.zhihu.com/p/41535500" class="internal">TiDB Robot:TiDB 源码阅读(十五) Sort Merge Join</a></p><p data-pid="abptL6Oa"><b>OpenGauss</b></p><p data-pid="0SDJPZJB">优化器框架:System-R Style框架</p><p data-pid="TakKQGNF">《openGauss数据库源码解析》</p><p data-pid="T2GBrkHG">《<b>openGauss数据库核心技术</b>》</p><p data-pid="4v4Tb798"><u><a href="https://zhuanlan.zhihu.com/p/391863470" class="internal">openGauss数据库源码解析系列文章——SQL引擎源码解析(一)</a></u></p><p data-pid="1Gz7Hac9"><u><a href="https://zhuanlan.zhihu.com/p/365964427" class="internal">openGauss数据库核心技术-SQL引擎(1)</a></u></p><p data-pid="aCnSnNJ9"><u><a href="https://zhuanlan.zhihu.com/p/365970257" class="internal">openGauss数据库核心技术-SQL引擎(2)</a></u></p><p data-pid="ePLfIMgZ"><u><a href="https://zhuanlan.zhihu.com/p/572857915" class="internal">解密openGauss数据库中的函数依赖关系</a></u></p><p data-pid="ve5XSGnS">另外:/src/gausskernel/dbmind/db4ai/目录下是AI相关的内容,这几年陆续出现了一些DB4AI的论文,但开源的代码不太多</p><p data-pid="3ZiZHsV5"><b>Apache Calcite</b></p><p data-pid="gfBoIjGA">优化器框架:Volcano/Cascades框架</p><p data-pid="rKWbUmqx"><a href="https://zhuanlan.zhihu.com/p/602840722" class="internal">LakeShen:Apache Calcite 和 SQL 引擎学习资料总结</a></p><p data-pid="8tTf4sBx"><b>AnalyticDB</b></p><p data-pid="CwVWR84-">优化器框架:Volcano/Cascades框架</p><p data-pid="_rs62TVL"><a href="https://link.zhihu.com/?target=https%3A//developer.aliyun.com/article/751481" class=" wrap external" target="_blank" rel="nofollow noreferrer">独家揭秘 | 阿里云分析型数据库AnalyticDB新一代CBO优化器技术-阿里云开发者社区</a></p><p data-pid="0liN9SHQ"><b>Apache Doris</b></p><p data-pid="lBtt3bnx">优化器框架:Volcano/Cascades框架</p><p data-pid="7DnvVT1i"><a href="https://link.zhihu.com/?target=https%3A//www.modb.pro/db/105847" class=" wrap external" target="_blank" rel="nofollow noreferrer">Apache Doris Join 实现与调优实践</a></p><p data-pid="drvZ8MO-"><u><a href="https://zhuanlan.zhihu.com/p/426919326" class="internal">悄悄学习Doris,偷偷惊艳所有人 | Apache Doris四万字小总结</a></u></p><p data-pid="PVamYcxv"><u><a href="https://link.zhihu.com/?target=https%3A//www.slidestalk.com/doris.apache/SQL82045" class=" wrap external" target="_blank" rel="nofollow noreferrer">第四讲-一条SQL的执行过程</a></u></p><p data-pid="BtK9ZJRj"><u><a href="https://link.zhihu.com/?target=https%3A//www.slidestalk.com/doris.apache/doris75626" class=" wrap external" target="_blank" rel="nofollow noreferrer">Apache Doris源码阅读与解析第8讲《查询优化器讲解》</a></u></p><p data-pid="sXQQE2a8"><u><a href="https://zhuanlan.zhihu.com/p/454141438" class="internal">Apache Doris 向量化技术实现与后续规划</a></u></p><p data-pid="ZiFIXjuf"><b>StarRocks</b></p><p data-pid="xCLtxsaV">优化器框架:Volcano/Cascades框架</p><ol><li data-pid="nKzf059B"><u><a href="https://link.zhihu.com/?target=https%3A//blog.bcmeng.com/post/starrocks-source-code-1.html" class=" wrap external" target="_blank" rel="nofollow noreferrer">StarRocks 源码导读一</a></u></li><li data-pid="s3I3oEJy"><u><a href="https://zhuanlan.zhihu.com/p/577956480" class="internal">StarRocks 优化器代码导读</a></u></li><li data-pid="24fcJmld"><u><a href="https://link.zhihu.com/?target=https%3A//blog.bcmeng.com/post/starrocks-internal.html" class=" wrap external" target="_blank" rel="nofollow noreferrer">StarRocks 技术原理资料汇总</a></u></li><li data-pid="6TinDPZF"><u><a href="https://zhuanlan.zhihu.com/p/579978445" class="internal">StarRocks Join Reorder 源码解析</a></u></li><li data-pid="4aMyzVGI"><u><a href="https://zhuanlan.zhihu.com/p/580164199" class="internal">StarRocks 技术内幕 | Join 查询优化</a></u></li><li data-pid="Tt_XEwE0"><u><a href="https://zhuanlan.zhihu.com/p/582214743" class="internal">StarRocks 统计信息和 Cost 估算</a></u></li><li data-pid="9UoamJkF">StarRocks Parser 源码解析<a href="https://zhuanlan.zhihu.com/p/560685391" class="internal"> 142</a></li><li data-pid="EnKOYYhC">StarRocks Analyzer 源码解析<a href="https://zhuanlan.zhihu.com/p/575973738" class="internal"> 46</a></li><li data-pid="c6BHKpHv">StarRocks 优化器代码导读<a href="https://zhuanlan.zhihu.com/p/577956480" class="internal"> 56</a></li><li data-pid="YbT2WdhK">StarRocks Join Reorder 源码解析<a href="https://zhuanlan.zhihu.com/p/579978445" class="internal"> 32</a></li><li data-pid="yoXjTSRF">StarRocks 统计信息和 Cost 估算<a href="https://zhuanlan.zhihu.com/p/582214743" class="internal"> 23</a></li><li data-pid="8bUnPT6q">StarRocks 如何添加一个优化规则<a href="https://zhuanlan.zhihu.com/p/583997914" class="internal"> 20</a></li><li data-pid="IFFagmB_">StarRocks 如何添加一个 Operator?<a href="https://zhuanlan.zhihu.com/p/586331147" class="internal"> 28</a></li><li data-pid="ciMVsjhf">StarRocks 查询调度源码解析<a href="https://zhuanlan.zhihu.com/p/588406885" class="internal"> 23</a></li><li data-pid="LxmpdToJ">StarRocks Scan 算子源码解析<a href="https://zhuanlan.zhihu.com/p/590275534" class="internal"> 33</a></li><li data-pid="q3u7L53m">StarRocks 聚合算子源码解析<a href="https://zhuanlan.zhihu.com/p/592058276?" class="internal"> 18</a></li><li data-pid="_RIPBX4t">StarRocks Hash Join 源码解析<a href="https://zhuanlan.zhihu.com/p/593611907" class="internal"> 36</a></li><li data-pid="-y-YDoaM">StarRocks Sort 算子源码解析<a href="https://zhuanlan.zhihu.com/p/595225671" class="internal"> 17</a></li><li data-pid="FoY8lrRt">StarRocks Exchange 算子源码解析<a href="https://zhuanlan.zhihu.com/p/596838323" class="internal"> 18</a></li><li data-pid="7hm3QASw">StarRocks 窗口算子源码解析<a href="https://zhuanlan.zhihu.com/p/598630498" class="internal"> 6</a></li><li data-pid="b-Hd9c-m">StarRocks Set 算子源码解析<a href="https://zhuanlan.zhihu.com/p/602915147" class="internal"> 4</a></li><li data-pid="1jEx-cwX">StarRocks Runtime Filter 源码解析<a href="https://zhuanlan.zhihu.com/p/605085563" class="internal"> 11</a></li><li data-pid="3SqQ3y_u"><a href="https://zhuanlan.zhihu.com/p/606924307" class="internal">StarRocks 如何新增一个数据类型 7</a></li></ol><p class="ztext-empty-paragraph"><br/></p><p data-pid="zLWgh-48">7-23 上述引自:</p><p data-pid="OgbG-avA"><a href="https://link.zhihu.com/?target=https%3A//forum.mirrorship.cn/t/topic/4356" class=" wrap external" target="_blank" rel="nofollow noreferrer">【源码解析】StarRocks 查询优化系列文章</a></p>
<p data-pid="_ttWOQ_I">最近面试一些小朋友,简历上赫然写着“擅长MySQL数据库优化”。</p><p data-pid="6Cw1v8F-">然后,我每每好奇地问上两句,你都用了什么方式做的数据库优化啊,基本上千篇一律地回复就是三个字:“加索引。”(手动狗头)</p><p data-pid="rJn8UHTZ">下面跟大家成体系化地详谈一下,MySQL数据库的优化方式有哪些?<br/></p><p data-pid="1AwWk2E8">既然谈到优化,一定想到要从多个维度进行优化。</p><p data-pid="9C4sia3R"><b>这里的优化维度有四个:硬件配置、参数配置、表结构设计和SQL语句及索引。</b></p><p data-pid="kcVMWIcf">其中 SQL 语句相关的优化手段是最为重要的。<br/></p><p data-pid="g4uJ13UQ">硬件方面的优化可以有 <b>对磁盘进行扩容、将机械硬盘换为SSD,或是把CPU的核数往上提升一些,增强数据库的计算能力,或是把内存扩容了,让Buffer Pool能吃进更多数据,</b> 等等。但这个优化手段成本最高,但见效最快。</p><p data-pid="Itb2FebJ">有句话怎么说的来着,能通过硬件升级来解决的事情,千万别碰代码。哈哈。<br/></p><p data-pid="ZOglG0j4">MySQL 会在内存中保存一定的数据,通过 <b>LRU(最近最少使用)算法</b>将不常访问的数据保存在硬盘文件中。尽可能的扩大内存中的数据量,将数据保存在内存中,从内存中读取数据,可以提升 MySQL 性能。</p><p data-pid="QyBe0ilf">MySQL 使用优化过后的 LRU 算法:</p><blockquote data-pid="Q5IzJzAY">普通LRU:末尾淘汰法,新数据从链表头部加入,释放空间时从末尾淘汰<br/>改进LRU:链表分为new和old两个部分,加入元素时并不是从表头插入,而是从中间 midpoint位置插入,如果数据很快被访问,那么page就会向new列表头部移动,如果 数据没有被访问,会逐步向old尾部移动,等待淘汰。每当有新的page数据读取到buffer pool时,InnoDb引擎会判断是否有空闲页,是否足够,如果有就将free page从free list列表删除,放入到LRU列表中。没有空闲页,就会根据LRU算法淘汰LRU链表默认的页,将内存空间释放分配给新的页。</blockquote><p data-pid="1d5yvI8W">LRU 算法针对的是 MySQL 内存中的结构,这里有个区域叫 <b>Buffer Pool(缓冲池)</b> 作为数据读写的缓冲区域。把这个区域进行相应的扩大即可提升性能,当然这个参数要针对服务器硬件的实际情况进行调整。</p><p data-pid="-YEiaSdb">通过以下命令可以查看相应的BufferPool的相关参数:</p><div class="highlight"><pre><code class="language-text">show global status like 'innodb_buffer_pool_pages_%'</code></pre></div><figure data-size="normal"><img src="https://pic1.zhimg.com/v2-16ca72cffbd8a4667edaa4df6132edf8_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="539" data-rawheight="280" data-original-token="v2-16ca72cffbd8a4667edaa4df6132edf8" class="origin_image zh-lightbox-thumb" width="539" data-original="https://pic1.zhimg.com/v2-16ca72cffbd8a4667edaa4df6132edf8_r.jpg?source=1940ef5c"/></figure><p data-pid="Y0xmhYZh">输入以下命令可以查看 BufferPool 的大小:</p><div class="highlight"><pre><code class="language-text">show variables like "%innodb_buffer_pool_size%"</code></pre></div><p data-pid="TkrbcAbn">在这里我们可以修改这个参数的值,如果该服务器是 MySQL 专用的服务器,我们可以 <b>修改为总内存的 60%~80%</b> ,当然不能影响系统程序的运行。</p><p data-pid="GaJU5wXz">这个参数是只读的,可以在 MySQL 的配置文件(my.cnf 或 my.ini)中进行修改。Linux 的配置文件为 <b>my.cnf</b>。</p><div class="highlight"><pre><code class="language-text"># 修改缓冲池大小为750M
innodb_buffer_pool_size=750M</code></pre></div><p data-pid="smschUQN">数据预热相当于将磁盘中的数据提前放入 BufferPool 内存缓冲池内。一定程度提升了读取速度。</p><p data-pid="g1KODxnB">对于 InnoDB,这里提供一份预热 SQL 脚本:</p><div class="highlight"><pre><code class="language-text">#mysql5.7版本中,如果DISTINCT和order by一起使用将会报3065错误,sql语句无法执行。这是由于5.7版本语法比之前版本语法要求更加严格导致的。
#推荐在mysql的配置文件my.cnf文件(linux)/my.ini文件(window) 的mysqld中增加或者修改sql_model配置选项
#sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
#重启后生效
SELECT DISTINCT
CONCAT('SELECT ',rowlist,' FROM ',db,'.',tb,
' ORDER BY ',rowlist,';') selectSql
FROM
(
SELECT
engine,table_schema db,table_name tb,
index_name,GROUP_CONCAT(column_name ORDER BY seq_in_index) rowlist
FROM
(
SELECT
B.engine,A.table_schema,A.table_name,
A.index_name,A.column_name,A.seq_in_index
FROM
information_schema.statistics A INNER JOIN
(
SELECT engine,table_schema,table_name
FROM information_schema.tables WHERE
engine='InnoDB'
) B USING (table_schema,table_name)
WHERE B.table_schema NOT IN ('information_schema','mysql')
ORDER BY table_schema,table_name,index_name,seq_in_index
) A
GROUP BY table_schema,table_name,index_name
) AA
ORDER BY db,tb;</code></pre></div><p data-pid="KQPcxh98">(1)增大 redo log,减少落盘次数:</p><p data-pid="fwqWuTA2">redo log 是重做日志,用于保证数据的一致,减少落盘相当于减少了系统 IO 操作。</p><p data-pid="maoqNoNV">innodb_log_file_size 设置为 0.25 * innodb_buffer_pool_size</p><p data-pid="vaFtkgyv">(2)通用查询日志、慢查询日志可以不开 ,binlog 可开启。</p><p data-pid="qJgqcA4u">通用查询和慢查询日志也是要落盘的,可以根据实际情况开启,如果不需要使用的话就可以关掉。binlog 用于恢复和主从复制,这个可以开启。</p><p data-pid="QV_CfOKX">查看相关参数的命令:</p><div class="highlight"><pre><code class="language-text"># 慢查询日志
show variables like 'slow_query_log%'
# 通用查询日志
show variables like '%general%';
# 错误日志
show variables like '%log_error%'
# 二进制日志
show variables like '%binlog%';</code></pre></div><p data-pid="OiUlxnXN">(3)写 redo log 策略 innodb_flush_log_at_trx_commit 设置为 0 或 2</p><p data-pid="I83ycv_r">对于不需要强一致性的业务,可以设置为 0 或 2。</p><ul><li data-pid="L2tEw4od">0:每隔 1 秒写日志文件和刷盘操作(写日志文件 LogBuffer --> OS cache,刷盘 OS cache --> 磁盘文件),最多丢失 1 秒数据</li><li data-pid="EU55aRCj">1:事务提交,立刻写日志文件和刷盘,数据不丢失,但是会频繁 IO 操作</li><li data-pid="BuRhwiIy">2:事务提交,立刻写日志文件,每隔 1 秒钟进行刷盘操作</li></ul><p data-pid="iplW6eaL"><b>back_log</b></p><p data-pid="VRmQWp7c">back_log值可以指出在MySQL暂时停止回答新请求之前的短时间内多少个请求可以被存在堆栈中。也就是说,如果MySQL的连接数据达到max_connections时,新来的请求将会被存在堆栈中,以等待某一连接释放资源,该堆栈的数量即back_log,如果等待连接的数量超过back_log,将不被授予连接资源。可以从默认的50升至500。</p><p data-pid="98jjFzYj"><b>wait_timeout</b></p><p data-pid="vtmDmYd6">数据库连接闲置时间,闲置连接会占用内存资源。可以从默认的8小时减到半小时。</p><p data-pid="p0zdIvhQ"><b>max_user_connection</b></p><p data-pid="OJMYOcUz">最大连接数,默认为0无上限,最好设一个合理上限。</p><p data-pid="nJO4CWSF"><b>thread_concurrency</b></p><p data-pid="qvonR6mY">并发线程数,设为CPU核数的两倍。</p><p data-pid="Z9SiKi_G"><b>skip_name_resolve</b></p><p data-pid="VYfqfOya">禁止对外部连接进行DNS解析,消除DNS解析时间,但需要所有远程主机用IP访问。</p><p data-pid="Jpw8ZkI7"><b>key_buffer_size</b></p><p data-pid="fgDLg6a2">索引块的缓存大小,增加会提升索引处理速度,对MyISAM表性能影响最大。对于内存4G左右,可设为256M或384M,通过查询show status like 'key_read%',保证key_reads / key_read_requests在0.1%以下最好。</p><p data-pid="dR4p0ZDt"><b>innodb_buffer_pool_size</b></p><p data-pid="sn6LQfiJ">缓存数据块和索引块,对InnoDB表性能影响最大。通过查询show status like 'Innodb_buffer_pool_read%',保证 (Innodb_buffer_pool_read_requests – Innodb_buffer_pool_reads) / Innodb_buffer_pool_read_requests越高越好。</p><p data-pid="kqVRwoz3"><b>innodb_additional_mem_pool_size</b></p><p data-pid="WmWe3zYk">InnoDB存储引擎用来存放数据字典信息以及一些内部数据结构的内存空间大小,当数据库对象非常多的时候,适当调整该参数的大小以确保所有数据都能存放在内存中提高访问效率,当过小的时候,MySQL会记录Warning信息到数据库的错误日志中,这时就需要该调整这个参数大小。</p><p data-pid="hGNa7wua"><b>innodb_log_buffer_size</b></p><p data-pid="m3r0tWk1">InnoDB存储引擎的事务日志所使用的缓冲区,一般来说不建议超过32MB。</p><p data-pid="2nUQr5B_"><b>query_cache_size</b></p><p data-pid="vtxhEJ0r">缓存MySQL中的ResultSet,也就是一条SQL语句执行的结果集,所以仅仅只能针对select语句。当某个表的数据有任何变化,都会导致所有引用了该表的select语句在Query Cache中的缓存数据失效。所以,当我们数据变化非常频繁的情况下,使用Query Cache可能得不偿失。根据命中率(Qcache_hits/(Qcache_hits+Qcache_inserts)*100))进行调整,一般不建议太大,256MB可能已经差不多了,大型的配置型静态数据可适当调大。可以通过命令show status like 'Qcache_%'查看目前系统Query catch使用大小。</p><p data-pid="S_bhj7HE"><b>read_buffer_size</b></p><p data-pid="gOMrEAPa">MySQL读入缓冲区大小。对表进行顺序扫描的请求将分配一个读入缓冲区,MySQL会为它分配一段内存缓冲区。如果对表的顺序扫描请求非常频繁,可以通过增加该变量值以及内存缓冲区大小来提高其性能。</p><p data-pid="P7sKj0xB"><b>sort_buffer_size</b></p><p data-pid="-CNBA4iK">MySQL执行排序使用的缓冲大小。如果想要增加ORDER BY的速度,首先看是否可以让MySQL使用索引而不是额外的排序阶段。如果不能,可以尝试增加sort_buffer_size变量的大小。</p><p data-pid="P227Dn_I"><b>read_rnd_buffer_size</b></p><p data-pid="WRowdYO8">MySQL的随机读缓冲区大小。当按任意顺序读取行时(例如按照排序顺序),将分配一个随机读缓存区。进行排序查询时,MySQL会首先扫描一遍该缓冲,以避免磁盘搜索,提高查询速度,如果需要排序大量数据,可适当调高该值。但MySQL会为每个客户连接发放该缓冲空间,所以应尽量适当设置该值,以避免内存开销过大。</p><p data-pid="pRKVUroT"><b>record_buffer</b></p><p data-pid="GKq-5HMj">每个进行一个顺序扫描的线程为其扫描的每张表分配这个大小的一个缓冲区。如果你做很多顺序扫描,可能想要增加该值。</p><p data-pid="1X0gRz2o"><b>thread_cache_size</b></p><p data-pid="JlP4Biqn">保存当前没有与连接关联但是准备为后面新的连接服务的线程,可以快速响应连接的线程请求而无需创建新的。</p><p data-pid="SuJT3Zpq"><b>table_cache</b></p><p data-pid="p2XJf5q_">类似于thread_cache _size,但用来缓存表文件,对InnoDB效果不大,主要用于MyISAM。<br/></p><p data-pid="Y-ZD1qiJ">设计聚合表,一般针对于统计分析功能,或者实时性不高的需求(报表统计,数据分析等系统),这是一种空间 + 时延性换时间的思想。</p><p data-pid="ucjfzFst">为减少关联查询,创建合理的冗余字段(创建冗余字段还需要注意数据一致性问题),当然,如果冗余字段过多,对系统复杂度和插入性能会有影响。</p><p data-pid="H5dhsGaZ">分表分为垂直拆分和水平拆分两种。</p><p data-pid="7EL_jZCe">垂直拆分,适用于字段太多的大表,比如:一个表有100多个字段,那么可以把表中经常不被使用的字段或者存储数据比较多的字段拆出来。</p><p data-pid="YSZmwwRg">水平拆分,比如:一个表有5千万数据,那按照一定策略拆分成十个表,每个表有500万数据。这种方式,除了可以解决查询性能问题,也可以解决数据写操作的热点征用问题。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="Pqn0wiMu">数据库中的表越小,在它上面执行的查询也就会越快。因此,在创建表的时候,为了获得更好的性能,我们可以将表中字段的宽度设得尽可能小。</p><ul><li data-pid="Tvsyi0Sz">使用可以存下数据最小的数据类型,合适即可</li><li data-pid="-BDJVuIr">尽量使用TINYINT、SMALLINT、MEDIUM_INT作为整数类型而非INT,如果非负则加上UNSIGNED;</li><li data-pid="0BjzVMk_">VARCHAR的长度只分配真正需要的空间;</li><li data-pid="-wvZiCE0">对于某些文本字段,比如"省份"或者"性别",使用枚举或整数代替字符串类型;在MySQL中, ENUM类型被当作数值型数据来处理,而数值型数据被处理起来的速度要比文本类型快得多</li><li data-pid="36bGUiOF">尽量使用TIMESTAMP而非DATETIME;</li><li data-pid="how_--wU">单表不要有太多字段,建议在20以内;</li><li data-pid="dC_q7s9q">尽可能使用 not null 定义字段,null 占用4字节空间,这样在将来执行查询的时候,数据库不用去比较NULL值。</li><li data-pid="k3kQ_Qk2">用整型来存IP。</li><li data-pid="iTsI2ed1">尽量少用 text 类型,非用不可时最好考虑拆表。</li></ul><p data-pid="ReVdZS2I">如果发现SQL查询比较慢,可以开启慢查询日志进行排查。</p><div class="highlight"><pre><code class="language-text"># 开启全局慢查询日志
SET global slow_query_log=ON;
# 设置慢查询日志文件名
SET global slow_query_log_file='slow-query.log';
# 记录未使用索引的SQL
SET global log_queries_not_using_indexes=ON;
# 慢查询的时间阈值,默认10秒
SET long_query_time=10;</code></pre></div><p data-pid="HcLKBvp6">注:索引并不是越多越好,要根据查询有针对性的创建。</p><ul><li data-pid="HXSlOdXL">单表查询:哪个列作查询条件,就在该列创建索引</li><li data-pid="m82nxuME">多表查询:left join 时,索引添加到右表关联字段;right join 时,索引添加到左表关联字段</li><li data-pid="avmpAr9M">不要对索引列进行任何操作(计算、函数、类型转换)</li><li data-pid="Zj-R1xeY">索引列中不要使用 !=,<> 非等于</li><li data-pid="dA5F6-4w">字符字段只建前缀索引,最好不要做主键;</li><li data-pid="CFrHKIHj">尽量不用UNIQUE,由程序保证约束</li><li data-pid="eMx-HLl0">不用外键,由程序保证约束</li><li data-pid="5xixXVW0">索引列不要为空,且不要使用 is null 或 is not null 判断</li><li data-pid="CJsmvoyS">索引字段是字符串类型,查询条件的值要加''单引号,避免底层类型自动转换</li></ul><p data-pid="eEDV8cRP">这里对explain的结果进行简单说明:</p><ul><li data-pid="pGyiqHX_">select_type:查询类型</li><ul><li data-pid="BacT8eAK">SIMPLE 简单查询</li><li data-pid="XxVR3yzB">PRIMARY 最外层查询</li><li data-pid="rjmllnSM">UNION union后续查询</li><li data-pid="bWdySQhv">SUBQUERY 子查询</li></ul><li data-pid="bmUYYXqs">type:查询数据时采用的方式</li><ul><li data-pid="9K4UOfUL">ALL 全表<b>(性能最差)</b></li><li data-pid="91wEBcXC">index 基于索引的全表</li><li data-pid="s8M7yGOH">range 范围 (< > in)</li><li data-pid="URqE2WHs">ref 非唯一索引单值查询</li><li data-pid="ol6tCaYh">const 使用主键或者唯一索引等值查询</li></ul><li data-pid="QsmKxFED">possible_keys:可能用到的索引</li><li data-pid="jiEmb1Wg">key:真正用到的索引</li><li data-pid="Z2lua1G0">rows:预估扫描多少行记录</li><li data-pid="6V1gK5AP">key_len:使用了索引的字节数</li><li data-pid="Iei-mFQy">Extra:额外信息</li><ul><li data-pid="0p9pmd-e">Using where 索引回表</li><li data-pid="Jvwyqrhw">Using index 索引直接满足条件</li><li data-pid="iI5Ir3-w">Using filesort 需要排序</li><li data-pid="TSYr2OOG">Using temprorary 使用到临时表</li></ul></ul><p data-pid="6ypCU9QW">对于以上的几个列,我们重点关注的是type,最直观的反映出SQL的性能。</p><p data-pid="4HVZxNfh">一条sql只能在一个cpu运算;大语句拆小语句,减少锁时间;一条大sql可以堵死整个库。</p><p data-pid="fkJDO_VW">SELECT id FROM t WHERE num BETWEEN 1 AND 5;</p><p data-pid="03rSxYcq">MySQL对于IN做了相应的优化,即将IN中的常量全部存储在一个数组里面,而且这个数组是排好序的。如果数值较多,需要在内存进行排序操作,产生的消耗也是比较大的。</p><p data-pid="1tYip3fh">SELECT * 增加很多不必要的消耗(CPU、IO、内存、网络带宽);减少了使用覆盖索引的可能性。</p><p data-pid="rDrVEecE">limit 相当于截断查询。</p><p data-pid="0YmENbAZ">例如:对于select * from user limit 1; 虽然进行了全表扫描,但是limit截断了全表扫描,从0开始取了1条数据。</p><p data-pid="UFLikX6q">排序的字段建立索引在排序的时候也会用到</p><p data-pid="2ywdF38b">union和union all的差别就在于union会对数据做一个distinct的动作,而这个distanct动作的速度则取决于现有数据的数量,数量越大则时间也越慢。而对于几个数据集,要确保数据集之间的数据互相不重复,基本是O(n)的算法复杂度。</p><p data-pid="qH02GjvR">如果是exists,那么以外层表为驱动表,先被访问,如果是IN,那么先执行子查询。所以IN适合于外表大而内表小的情况;EXISTS适合于外表小而内表大的情况。</p><p data-pid="HgpUr-mf">limit m n,其中的m偏移量尽量小。m越大查询越慢。</p><p data-pid="VnktbegG">例如:like '%name'或者like '%name%',这种查询会导致索引失效而进行全表扫描。但是可以使用like 'name%',这种会使用到索引。</p><p data-pid="e-fE2lwW">这种不会使用到索引:</p><div class="highlight"><pre><code class="language-text">select user_id,user_project from user_base where age*2=36;</code></pre></div><p data-pid="WMBfj7sC">可以改为:</p><div class="highlight"><pre><code class="language-text">select user_id,user_project from user_base where age=36/2;</code></pre></div><p data-pid="UR2UaQFJ">任何对列的操作都将导致表扫描,它包括数据库函数、计算表达式等等,查询时要尽可能将操作移至等号右边。</p><p data-pid="SYDgPr12">where 子句中出现的 column 字段要和数据库中的字段类型对应</p><p data-pid="51yvbFwH">有的时候 MySQL 优化器采取它认为合适的索引来检索 SQL 语句,但是可能它所采用的索引并不是我们想要的。这时就可以采用 forceindex 来强制优化器使用我们制定的索引。</p><p data-pid="fjrrZxxL">对于联合索引来说,如果存在范围查询,比如between、>、<等条件时,会造成后面的索引字段失效。</p><p data-pid="1CSWInCx">因为使用 join,MySQL 不会在内存中创建临时表。</p><p data-pid="YPDCPrEn">使用小表驱动大表,例如使用inner join时,优化器会选择小表作为驱动表</p><p data-pid="lmu6_31y">如:以 A,B 两表为例,两表通过 id 字段进行关联。</p><div class="highlight"><pre><code class="language-text">#当 B 表的数据集小于 A 表时,用 in 优化 exist;使用 in ,两表执行顺序是先查 B 表,再查 A 表
select * from A where id in (select id from B)
#当 A 表的数据集小于 B 表时,用 exist 优化 in;使用 exists,两表执行顺序是先查 A 表,再查 B 表
select * from A where exists (select 1 from B where B.id=A.id)</code></pre></div><p data-pid="mkC471Xi"><br/>上面都是一些常规的优化方法,我们还可以使用:主从和分库。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="LCz3XYvP">主从相对比较简单,从运维层面搭建好从库后,工程师要做的就是制定路由策略。</p><p data-pid="B-nlFGZ8">路由策略有如下两种:</p><p data-pid="dHaGaEli">读写分离模式,所有写操作和对实时性要求较高的by id查询走主库,剩下的都走从库,从库采用Round Robin模式。</p><p data-pid="fOsFtAj9">链路隔离模式:写操作和核心操作对应的SQL走主库,耗时大、非核心操作的SQL走从库。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="EIYDv10U">分库策略需要根据业务场景制定,最常见的有两种:按照年月分库和按照角色分库。</p><p data-pid="GGDxhmWM">按照角色分库,最经典的就是淘宝基于订单的买家库和卖家库。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="JOPLg6SJ">整体来讲,这篇应该很全面了,如果有新的方案策略,我再往上添加。</p>
<p></p><p class="ztext-empty-paragraph"><br/></p><p data-pid="FJBQl_rl">在基于成本的查询优化器中,找到连接操作的最佳执行顺序是一项关键任务。由于对于给定查询存在许多可能的连接树,因此每个有效连接树的连接(树)枚举算法的开销应尽量小。在以团状查询图形为例的情况下,已知的自顶向下算法在每个连接树上的复杂度为Θ(n^2),其中n是关系的数量。本文介绍了一种在这种情况下具有O(1)复杂度的算法。</p><p data-pid="dmB6yNe1">我们通过实验证明,这一更加理论性的结果确实对其他非团状设置的性能产生了很大的影响,特别是对于循环查询图形而言。此外,我们评估了我们的新算法的性能,并与文献中描述的最佳自顶向下和自底向上算法进行了比较。</p><p data-pid="_zPVr7XP">对于支持类似SQL的声明性查询语言的数据库管理系统(DBMS),查询优化器是一个至关重要的软件组件。查询的声明性特性允许将其转换为许多等价的执行计划。从所有可选方案中选择合适的计划的过程被称为查询优化。此选择的基础是成本模型和数据的统计信息。计划的成本对于执行计划中连接操作的执行顺序至关重要,因为具有不同连接顺序的计划的运行时间可能相差数个数量级。对于所有可能的操作符树的最优解的穷尽搜索是计算上不可行的。为了降低复杂性,必须限制搜索空间。在本文讨论的优化问题中,应用了一种被广泛接受的启发式方法:我们考虑所有可能的浓密连接树[1],但从搜索中排除了交叉产品,假设所有考虑的查询都跨越一个连通查询图[2]。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="tQZAvInx">在设计查询优化器时,有两种策略可以找到最佳的连接顺序:自底向上的连接枚举(通过动态规划)和自顶向下的连接枚举(通过记忆化)。这两种方法(自然而然地)都必须探索相同的搜索空间,并面临相同的挑战。让我们简要回顾一下这个挑战。这需要一些准备工作。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="40dHvi7j">对于每个导致连接的关系子集S形成一个连通子图(简称csg),必须构建最佳的连接树。为了确定给定关系子集S的最佳连接树,计划生成器必须枚举所有的划分(S1,S2),使得S=S1∪S2并且S1∩S2=?。此外,由于我们排除了交叉产品,S1和S2必须形成查询图的连通子图,并且必须存在两个关系R1∈S1和R2∈S2使得它们由一条边连接,即必须存在一个涉及R1和R2属性的连接谓词。让我们将这样的划分(S1,S2)称为csg-cmp-pair(或简称ccp)。用Ti表示Si的最佳计划。然后查询优化器必须考虑所有csg-cmp-pair(S1,S2)的计划T1 T2。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="qZ9mNO3j">一种生成关系集S的所有csg-cmp-pair的方法是考虑所有的子集S1?S,定义S2=S\\S1,然后检查上述条件。让我们称这样的过程为朴素的生成和测试(naive generate and test)或简称ngt。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="ZiwY9B1l">表I给出了对于n=5、10、15、20个关系的情况下的连通子图的数量(#csg)、csg-cmp-pair的数量(#ccp)以及朴素生成和测试算法(#ngt)生成的子集S1的数量。这些数字是通过分析确定的([2],[3]),但公式并不直观。因此,我们决定用一些具体的数字来说明我们的观点。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="4fRGTQgU"><b>挑战</b>: 朴素的生成和测试方法考虑的子集数量比csg-cmp-pair的数量高几个数量级。因此,这种方法效率太低,无法发挥作用(见IV-D节)。因此,挑战是只生成有效的csg-cmp-pair,并且以尽可能小的开销完成此操作。相当长的一段时间里,没有人知道如何高效地枚举csg-cmp-pair。在自底向上的连接枚举中,对于给定的集合,所有的连通子集已经被生成。因此,基于动态规划的枚举策略应该比基于生成和测试的策略更容易设计。Moerkotte和Neumann[3]提出了一种称为DPCCP的动态规划变体,可以在常量时间O(1)内生成csg-cmp-pair。DeHaan和Tompa接受了更大的挑战,并提出了一种用于自顶向下连接枚举的最小图切割分区算法MINCUTLAZY[4]。对于无环查询图,生成csg-cmp-pair的复杂度也是O(1)。然而,对于有环查询图,复杂度增加,并在完全图的情况下达到最大值,复杂度为O(n^2),其中n为关系的数量(见附录)。</p><p class="ztext-empty-paragraph"><br/></p><p class="ztext-empty-paragraph"><br/></p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-ecfc2d9f4340d89ac0c1e2f34e9e4272_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="956" data-rawheight="614" data-original-token="v2-fc81b4796d42ac3a6fa90f1c797580f9" class="origin_image zh-lightbox-thumb" width="956" data-original="https://picx.zhimg.com/v2-ecfc2d9f4340d89ac0c1e2f34e9e4272_r.jpg?source=1940ef5c"/></figure><p class="ztext-empty-paragraph"><br/></p><p data-pid="yW5Bb6fC"><b>幸运的观察: </b>连通子图的数量(#csg)远远小于csg-cmp-pair的数量。这非常幸运,因为(1)#csg是基数估计发生的次数,(2)#ccp是连接成本函数被评估的次数,而且(3)后者的成本比前者低几个数量级。实际上,典型的连接成本函数只需要几个算术操作即可进行评估[5]。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="ozO6Yb5i"><b>贡献</b>: 在本文中,我们提出了一种新的高效的自顶向下连接枚举算法,并评估了其性能。具体而言,我们:</p><p data-pid="eOt3vfnU">?对MINCUTLAZY进行了详细的复杂性分析,证明其在完全图查询中具有O(n^2)的复杂度,</p><p data-pid="PaiAs044">?提出了分支划分作为一种全新且易于实现的自顶向下连接枚举算法,</p><p data-pid="06uJUFMx">?通过理论分析表明,分支划分的复杂度对于无环图、环图和完全图查询都是O(1)的,</p><p data-pid="XbeofRVE">?进行了深入的性能评估,表明新的枚举算法几乎与DPCCP一样高效。</p><p data-pid="3-Eg6not">除了高效性之外,我们的新算法还具有一个重大优势,即它比DeHaan和Tompa开发的算法更容易实现。他们需要构建和维护一个复杂的数据结构,称为双连接树(biconnection tree),而我们只需要使用位向量进行集合操作,这可以轻松高效地实现。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="DsLHv6d8"><b>重要说明</b>. 让我们注意到,分支限界修剪对某些查询可能是有效的。然而,修剪对于所有自顶向下的算法都具有相同的优势。因此,我们决定在此忽略其影响。不考虑修剪的另一个优点是可以与自底向上的方法进行公平的原始性能比较,而自底向上的方法很难进行修剪。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="hzoNgWgY"><b>组织结构.</b> 本文的组织结构如下:第二节回顾了一些预备知识。第三节介绍了我们的新算法。第四节进行了全面的性能评估。第五节对论文进行了总结。附录包含了MINCUTLAZY的复杂度分析。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="03dIt8oe">在我们开始讨论之前,我们先介绍一些基础知识。在第一小节中,我们解释了一些重要概念,并介绍了自顶向下的连接枚举方法。我们提出了一种通用的记忆化算法,用于连接优化,可以与不同的连接子图对枚举策略相结合,我们也将其称为分区策略或算法。本节的最后部分介绍了朴素的生成和测试分区算法。</p><p data-pid="PXgY_CFZ">A. 重要概念</p><p data-pid="YD17A8Be">在本小节中,我们给出了一些定义,这些定义对于深入理解本文的工作是重要的。</p><p data-pid="8Qnh0j80">我们的重点是确定给定查询的最佳连接顺序。连接操作的执行顺序由物理代数的操作树指定。为了简化问题,我们希望从表示中抽象出来,并给出连接树的概念。连接树是一棵二叉树,其中叶子节点指定查询中引用的关系,内部节点指定两个关系的连接操作。连接树的边表示连接的关系集合。两个输入关系集合可以进行连接而无需考虑笛卡尔积的情况称为连接子图及其补充对,简称ccp[3]。</p><p data-pid="kgQM-j1C">定义 2.1: 设G=(V, E)是一个连接查询图,(S1,S2)是一个连接子图及其补充对(或称为ccp),如果满足以下条件:</p><p data-pid="8se0SG6Y">? S1中的S1?V引起了一个连接图G|S1,</p><p data-pid="cIGAEuoG">? S2中的S2?V引起了一个连接图G|S2,</p><p data-pid="TeO8878m">? S1∩S2=?,</p><p data-pid="mH9wRLSG">? 存在(v1,v2)∈E|v1∈S1∧v2∈S2。</p><p data-pid="XcYgPUlG">所有可能的ccp集合记为Pccp。我们引入cmp-csg对的概念来指定那些输入集合的对,如果连接,则它们会产生相同的输出集合。</p><p data-pid="s5kH4SU9">定义 2.2: 设G=(V, E)是一个连接查询图,S是一个集合,S?V引起了连接子图G|S。对于S1,S2?V,如果(S1,S2)是一个ccp,并且S1∪S2=S,那么称(S1,S2)是S的ccp。我们用Pccp(S)表示S的所有ccp集合。令Pcon(V)={S?V|G|S是连接的且|S|>1}表示V的所有连接子集的集合,那么有Pccp=∪S∈Pcon(V)Pccp(S)。</p><p data-pid="CkwAFC7E">如果(S1,S2)是一个ccp,那么(S2,S1)也是一个ccp,并且我们将它们视为对称对。我们对Psym</p><p data-pid="RLs0wXk1">ccp定义为所有ccp的集合,其中对称对仅计算一次,例如,(S1,S2)∈Psym ccp,如果maxindex(S1)≤maxindex(S2),或者(S2,S1)∈Psym ccp。我们不对选择两个对称对中的哪一个成为Psym ccp的成员施加限制,这是一种自由度。类似地,我们用Psym ccp (S)表示包含(S1,S2)或(S2,S1)的S的所有ccp的集合。由Ono和Lohman[2]给出的连接枚举的下界(见表I)对应于|Psym ccp|。</p><p data-pid="vZoe5ouA">因此,这解释了cliques的#ccp和#ngt之间差一个因子为两倍。</p><p data-pid="QWqa3R8I">接下来,我们定义了一个节点集合的邻域:</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="VrF4lgfl">定义 2.3: 设G=(V, E)是一个无向图,集合S?V的邻域定义为:</p><p data-pid="HXbcyOnl">N(S)={w∈(V\\S)|v∈S∧(v, w)∈E}。</p><p data-pid="TqhPj-ti">下一个定义是相当标准的,更多细节请参见[9]。</p><p data-pid="eerevdLY">定义 2.4: 设G=(V,E)是一个连接的无向图。双连通分量是G的一个连接子图GBCC</p><p data-pid="NxlkYcTU">i=(Vi,Ei),其中Vi={v|(v=u∨v=w)∧(v, w)∈Ei},Ei是满足任何两个不同的边(u, w)∈Ei和(x, y)∈Eilie在一个环上的最大集合,v0,v1,v2, ..., vl,其中</p><p data-pid="O8QbCDNJ">u=v0∧u=vl∧w=v1∧x=vj?1∧y=vj∧0<j<l和?0≤i<j<l vi,vj∈V∧vi=vj。如果不存在这样的环,则边(u, w)∈Ei,该顶点u, w ∈Vi构成一个双连通分量GBCCi=({u, w },{(u, w)})。</p><p data-pid="P3-tsfRV">DeHaan和Tompa的MINCUTLAZY使用了一个称为双连通树的数据结构。我们给出其定义:</p><p data-pid="QrcnIiTQ">定义 2.5: 设G=(V,E)是一个连接的无向图,BCC={GBCC1(V1,E1), ..., GBCCk(Vk,Ek)}是组成G的所有双连通分量的集合,其中V=1≤i≤kVi。对于任意的顶点t∈V,一个顶点节点集合Vvn和一个集合节点集合Vsn,其中Vtree=Vvn ∪Vsn且Vvn ∩Vsn=?,如果满足以下条件,我们称T=(Vtr ee,Etree,t)为一个双连通树:</p><p data-pid="Q4I2I9xm">? Vvn=V,</p><p data-pid="cM1jDw-D">? Vsn={sVi|s表示双连通分量GBCCi(Vi,Ei)的顶点集合Vi},</p><p data-pid="MLJ8gmm2">? 树边集合Etree={(sVi,v)|sVi∈Vsn ∧v∈Vi}。</p><p data-pid="9DiyaT4C">顶点t被称为T的根节点。</p><p data-pid="nkesOXkk">在双连通树T中,一个任意顶点v∈V的后代DT和祖先AT可以定义如下。</p><p data-pid="H7AnpsCb">DT(v)={u∈V|u出现在以v为根的子树中},</p><p data-pid="cXLppVCZ">AT(v)={u∈V|u是t?→v路径上的一个顶点节点}。</p><p data-pid="wbRLhfmM">B. 基本记忆化</p><p data-pid="qBK67XSQ">作为自顶向下连接枚举的介绍,我们给出了一种基本的记忆化变体,称为基本记忆化算法(MEMOIZATIONBASIC),通过使用朴素的分区算法进行实例化。在第一小小节中,我们介绍了通用的自顶向下算法。之后,我们解释了朴素的分区策略。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="98RJDX25">通用自顶向下连接枚举:我们的通用自顶向下连接枚举算法TDPLANGEN基于记忆化。我们在图1中给出了其伪代码。像动态规划一样,TDPLANGEN首先初始化原子关系的构建块(第2行)。然后,在第3行调用子例程TDPGSUB,该子例程通过递归遍历搜索空间。在根递归调用时,顶点集合Sc对应于查询图的顶点集合V。在TDPGSUB的每个递归步骤中,通过BUILDTREE(第3行)构建由两个优化的子连接树组成的一起包含S中的关系的所有可能连接树,并保留成本最低的连接树。我们通过在第2行中迭代Psym</p><p data-pid="YnTAvF-P">ccp (S)的元素来枚举S的优化子连接树。通过对TD PGSUB进行递归调用,我们可以得到包含S1或S2中的关系的两个优化子连接树。生成Psym</p><p data-pid="OU3JEl1S">ccp (S)是分区算法的任务。根据分区策略的选择,TDPLANGEN的整体性能可以相差数个数量级。</p><p data-pid="63hd5N-6">当|S|=1或已经为G|S调用了TDPGSUB时,递归下降停止。在这两种情况下,最优连接树已经得到。为了防止TDPGSUB重复计算最优树,我们在第1行中检查BestTree[S]。BestTree[S]返回对应于记忆表中的一个条目的引用。数据结构“记忆表”记录了为集合S生成的最优连接树。如果BestTree[S]等于NULL,表示首次以G|S作为输入调用TDPGSUB,并且尚未找到G|S的最优连接树。</p><p class="ztext-empty-paragraph"><br/></p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-9259bb084d9ce9c83a76f62efa979192_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1022" data-rawheight="836" data-original-token="v2-bdd995c62c0a77f60bdf7cf98b9596a6" class="origin_image zh-lightbox-thumb" width="1022" data-original="https://picx.zhimg.com/v2-9259bb084d9ce9c83a76f62efa979192_r.jpg?source=1940ef5c"/></figure><p class="ztext-empty-paragraph"><br/></p><p class="ztext-empty-paragraph"><br/></p><p class="ztext-empty-paragraph"><br/></p><figure data-size="normal"><img src="https://pic1.zhimg.com/v2-609956542dc6336cbb5d3bcf57157d98_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1036" data-rawheight="568" data-original-token="v2-a131f5564e32b318e2c2b8c70e7be257" class="origin_image zh-lightbox-thumb" width="1036" data-original="https://pic1.zhimg.com/v2-609956542dc6336cbb5d3bcf57157d98_r.jpg?source=1940ef5c"/></figure><p class="ztext-empty-paragraph"><br/></p><p class="ztext-empty-paragraph"><br/></p><p class="ztext-empty-paragraph"><br/></p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-2e4f236f03b095ad397180780f1b7b86_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1014" data-rawheight="492" data-original-token="v2-2a304e021984f0cb813fa37561ee625e" class="origin_image zh-lightbox-thumb" width="1014" data-original="https://picx.zhimg.com/v2-2e4f236f03b095ad397180780f1b7b86_r.jpg?source=1940ef5c"/></figure><p class="ztext-empty-paragraph"><br/></p><p class="ztext-empty-paragraph"><br/></p><p data-pid="6o-dvXyt">BUILDTREE的伪代码如图2所示。它用于比较属于相同G|S的连接树的成本。由于只枚举对称对(S1,S</p><p data-pid="jUQtMx8v">2)和(S2,S1)(T DPGSUB的第2行),我们需要构建两棵连接树(第1行和第4行),然后比较它们的成本。我们使用CREATETREE方法,它接受两个不相交的连接树作为参数,并将它们组合成一个新的连接树。如果必须考虑不同的连接实现,那么在所有可选方案中,必须构建最便宜的连接树。如果创建的连接树(第1行)比BestTree[S]便宜,或者尚未为S构建过连接树,那么BestTree[S]将与CurrentTree进行注册。对于构建第二棵树,我们只需交换参数(第4行)。同样,新连接树的成本与BestTree[S]的成本进行比较。只有当新连接树的成本更低时,BestTree[S]才与新连接树进行注册。请注意,由于第3行,BestTree[S]在第6行不能为NULL。同时估计两棵可能的连接树的成本而不是分别估计它们并进行比较更高效,例如,对于如[5]中给出的成本函数,其中card(Tx)≤card(Ty)?cost(Tx1Ty)≤cost(Ty1Tx)成立,其中card是元组或页面的数量,Tx、Ty是(中间)关系。</p><p data-pid="q2Fx7q-n">朴素分区:正如我们已经看到的,通用自顶向下枚举算法迭代了Psym ccp (S)的元素。现在,我们展示如何通过朴素的生成和测试策略计算Scan的ccp。我们将我们的算法称为朴素的生成和测试分区算法,给出其伪代码如图3所示。在第1行,枚举了V的所有2|V|?2个可能的非空且正确的子集。可以使用[6]中描述的方法快速进行子集枚举。我们要求从每个对称对中只发出一个。有很多可能的解决方案,但我们确保图中表示的最高索引的关系始终包含在补充集合V\\S中(第2行)。有三个条件必须满足,以使分区(S, V \\S)成为ccp。我们在第2行检查G|S和G|V\\S的连通性。第三个条件,即S需要与V\\S相连通,由输入的图为连接图的要求隐含地保证。</p><p class="ztext-empty-paragraph"><br/></p><p class="ztext-empty-paragraph"><br/></p><p data-pid="sA_6vUxH">DeHaan和Tompa[4]提出了文献中已知的用于无交叉乘积的茂密连接树搜索空间的最高效自顶向下连接枚举算法(附录A)。正如我们所展示的,对于链式、星型和循环查询,它的复杂度为O(1),对于团查询,复杂度为O(|S|2)(附录B)。在本节中,我们介绍一种名为MINCUTBRANCH的新的连接分区算法,并研究其复杂度。</p><p data-pid="RTOryvHf">A. 分支分区 - 概述</p><p data-pid="bN5cn5SL">本节介绍我们的新颖分区算法,称为分支分区,我们将其记作MINCUTBRANCH。分区算法由TDPGS UB调用,以计算给定连通顶点集S的所有可能的两个不相交互联集合(S1,S2)的CCPs。分支分区的输出是一个CCP集合Psym ccp(S),以便对称CCP仅发出一次。</p><p data-pid="3RzAMYew">在图4和5中,我们给出了算法的伪代码,其中包括PARTITIONMinCutBranch和MINCUTBRANCH。我们称其为带有TD前缀的泛型记忆化变体TDMINCUTBRANCH,以表示基于分支分区的自顶向下算法。</p><p data-pid="0NF7gC_B">算法的思路是通过其邻域N(C)的成员递归扩大集合C,从集合S中的任一顶点t∈S开始。通过这种方式,我们确保算法的每个执行实例中C都是连通的。如果在某个扩大C的实例中,它的补集S\\C在S中也是连通的,那么算法已经找到了S的CCP。此外,在发出CCP之前,C的补集分区还必须满足一些额外的约束:(1)仅发出对称CCP,(2)避免发出重复的CCP,(3)只要它们符合约束(1),就要计算S的所有CCP。</p><p data-pid="qEb82Xga">约束(1)是由于在PARTITIONMinCutBranch的第1行初始化分区算法期间任意选择的起始顶点t始终包含在C中,并且永远不会成为其补集的一部分。对于第二个约束,算法使用一个过滤器集合X来排除处理。在算法的每次递归自调用之后,用于扩大C的邻居v∈N(C)被添加到X中。稍后,我们将详细介绍这是如何工作的。对于约束(3),只需确保在扩大C时考虑S的所有可能连通子集。</p><p data-pid="TSKj29sm">检查补集的连通性会增加每个测试的线性开销。此外,对于某些场景(例如,考虑星型查询),当构造S的每个可能连通子集C时,会产生指数级的开销,因为大多数补集S\\C都不是连通的,并且以这种方式计算的分区(C,S \\C)不是有效的CCP。对于分支分区,我们提出了一种新颖的技术,可以确保不会生成不是CCP的分区。作为积极的副作用,可以消除对连通性的额外检查。</p><p data-pid="tcmcKt3v">在解释我们的技术之前,我们必须进行一些观察。从扩大C的递归过程中,我们知道C中成员的数量必须在每次迭代中增加一。此外,如果分区(C,S \\C)对于S来说不是CCP,则S\\C由k≥2个相互不相连的连接子集D1,D2,...,Dk?(S \\C)组成。因此,这些子集D1,D2,...,Dk只能与C相邻。令v1,v2,...,vl为C的邻域N(C)中的所有成员。那么对于1≤x≤k的每个Dx,必须包含至少一个这样的vy(其中1≤y≤l)并且k≤l成立。</p><p data-pid="ti14T7K6">当通过S \\C的成员扩大C时,第一个CCP将在将所有Dx(其中1≤x≤k)但一个与C连接时生成。</p><p data-pid="DfOfxZHp">在做出这些观察之后,我们准备解释我们的基本思想。关键原则是利用有关S \\C如何与MINCUTLAZY的子调用相关联的信息。因此,我们引入一个新的输入参数L和一个结果集R。单元素集合L包含最后一个通过父调用添加到C中的顶点v。子调用的结果集R包含最大化扩展且连接的集合Dx,满足L?R。注意,R作为MINCUTBRANCH的返回值的概念与分支分区必须发出的分区不同。我们通过将子调用的结果集Rtmp与L连接来计算R。但是,我们必须小心地只包含那些与L相邻的Rtmp。因此,我们需要区分N(L)和(N(C)\\(L)):只有满足N(L)∩Rtmp ≠ ?的Rtmp才能与R连接。</p><p data-pid="OHLOGRNi">为了利用与v相邻的连接集合Rtmp,我们将发出ccp的操作推迟到最后。当补集S\\C不连通时,我们不会通过使用除了一个Rtmp之外的所有Rtmp来扩大C,而是立即发出(S\\Rtmp,Rtmp)。请注意,如果S\\C连通,则只存在一个Rtmp,其中Rtmp=S \\C,并且(S\\Rtmp,Rtmp)=(C,S \\C)成立。我们已经说过,根据约束(3),必须将S的所有连通子集视为集合C的值。通过这种优化,跳过了某些S\\Rtmp的连通子集。由于我们仅避免了那些补集S\\(S\\Rtmp)=Rtmp不是一个连通集的S\\Rtmp,我们的优化仍足以满足约束(3)。</p><p data-pid="agNQKTEa">B. 算法详解</p><p data-pid="82snZumt">接下来,我们详细介绍图4和5中给出的伪代码。PARTITIONMinCutBranch第一次使用C=L={t}调用MINCUTBRANCH,其中t是任意顶点。这确保了约束(1),因为在MINCUTBRANCH的执行的任何实例中,Rtmp都不能包含t。在第1行和第2行,初始化结果集R和Rtmp。</p><p data-pid="eJbTs1D0">在处理C的邻居时,主要关注的是最近添加的顶点v∈L的邻居,因为它们对于计算返回值很重要。因此,在第3行引入了集合NL来存储一定需要处理的所有邻居,即L的所有邻居中不在X中的邻居。同时,只有当它们属于一个具有L邻居的子调用的结果集Rtmp时,才会探索C的其他邻居,这些邻居同时不是L的邻居。我们将此类别的邻居存储在集合NB中,其中包含C的所有邻居但不包含N(C)\\(L)中的邻居(第5行)。在处理既是L的邻居又是X的元素之前,必须格外小心,而集合X保存了在祖先调用中已处理的先前邻居。现在,只需要处理那些既是L的邻居又是X的元素并且不包含在Rtmp的任何结果集中的邻居。我们在第4行计算这些候选项,并将它们存储到NX中。决定是否处理包含在最后两个集合NB和NX中的其他邻居是在第6到29行的循环中动态决定的。</p><p data-pid="Tf_kW18a">循环(第6到29行)包含三种情况。要理解这些情况,我们必须了解由于我们的重复避免技术而存在的附加要求。如前所述,我们使用过滤器集合X来排除其成员作为MINCUTBRANCH的子调用中的新L被处理。此外,如果补集S\\Rtmp与X不是不相交的,那么(S\\Rtmp,Rtmp)是一个重复项,并且已经被发出。为了解释这一事实,我们将S\\(Rtmp∩X)中的成员表示为vold。我们知道vold∈N(C)必须成立,因为vold作为X的成员意味着vold作为Cold的邻居在MINCUTBRANCH的一个祖先调用中作为邻居进行处理。正如稍后将看到的那样,vold必须与S\\Cold中的v相连,其中Cold ? C。因此,从C=Cold ∪{vold}和L={vold}开始的递归下降必须在某个时候以相同的Rtmp返回。因此,分区(S\\Rtmp,Rtmp)已经被发出。我们在第24行实现了重复项测试,并在第27行发出ccp。</p><p data-pid="2sofgQGv">现在让我们来看看图7的链式查询示例。我们选择R0作为初始C。在MINCUTBRANCH的根调用中,我们首先处理R1。当子调用返回时,Rtmp等于{R1,R3}。在将R2作为第二个邻居处理之前,我们将R0添加到X中。在MINCUTBRANCH的下一个子调用中,其中C={R0,R2},L={R2},X={R0},使用C={R0,R2,R4},L={R4},X={R0}的进一步递归调用将返回一个R={R4}。但是,我们将错误地假设它是一个重复项,因为(S\\Rtmp)∩X≠?。为了解决这个问题,在选择一个尚未成为R的新邻居v时,需要将X重置为X(第12行)。因此,我们指定了NL,NB和NX的处理顺序,并定义了三种情况:在第7行检查第(1)种情况。如果子调用始于NL中的一个vx(情况(2))或NX中的一个vx(情况(3)),并且vy∈NL或vy∈NB是返回的Rtmp的一部分,则为真。由于我们使用的重复避免技术,我们无需保存其返回值并且无需发出分区,因为已经发出了。注意,子调用的排除的过滤器集合设置为X(第15行),我们必须在之前通过处理情况(2)或(3)在第12行设置或重置我们自己的X。在子调用返回后,我们将结果保存在Rtmp中。请注意,Rtmp ∩R=?。稍后在第28行,Rtmp与R连接。处理当前v之后,将其从其原始集合中删除,该集合要么是NL(第10行)或NB(第11行)。</p><p class="ztext-empty-paragraph"><br/></p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-34bc2581bb81f42b4581185d91dc48c2_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1022" data-rawheight="404" data-original-token="v2-7ec7d7e58ed67ec536e73b2a8b03a4a9" class="origin_image zh-lightbox-thumb" width="1022" data-original="https://picx.zhimg.com/v2-34bc2581bb81f42b4581185d91dc48c2_r.jpg?source=1940ef5c"/></figure><p class="ztext-empty-paragraph"><br/></p><p data-pid="BUucwthv">第20到26行解释了第III-C节中的内容。在返回调用之前,将L连接到最终结果集R。</p><p data-pid="mgxZ3SCP">C. 两种优化技术</p><p data-pid="xR58tTKW">第20到26行指定了两种不是对分支分区算法的要求的优化技术。第一种技术考虑到Rtmp包含X的元素的情况。在这种情况下,Rtmp的所有其他MINCUTBRANCH调用和它们与C的邻居的子调用,这些邻居与Rtmp不相交,都不能发出任何分区,因为它们生成的Rtmp必须与Rtmp不相交,以使S\\Rtmp不能再与X不相交(第24行)。但是,由于我们需要确保正确计算R,因此必须将那些我们希望避免对MINCUTBRANCH进行不必要调用的邻居添加到NX(第20行)中。</p><p data-pid="KBTswTzX">第二种优化技术避免了探索与X不相交的补集S\\Rtmp的所有其他C的邻居。如前所述,如果不从NL和NB中减去这些邻居,它们将在循环的下一次迭代中被处理,并且将符合第8行的条件。因此,对于第9行中调用的MINCUTBRANCH的所有结果调用都无法避免,尽管它们不会发出任何ccps。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="vNGuwEMM">D. 探索限制邻居</p><p data-pid="ABdqZ_fx">最后,我们解释一下REACHABLE。正如前面提到的,它的目的是返回与L相邻的最大扩展且连通的集合R。在REACHABLE的第1行,使用一个元素的结果集R来初始化,该结果集包含L。扩展R始于与C不相交且属于S的L的邻居集合。在第3至5行的while循环中,将前一次循环中与C不相交的邻居的邻居添加到R中。当没有顶点需要添加时,循环结束。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="mUeEpQFb">E. 两个示例</p><p data-pid="UUfhTPsb">我们通过两个示例来说明MINCUTBRANCH的执行过程。表II和表III展示了给定图7的链查询或图8的循环查询的执行步骤。第一列名为level用于跟踪递归级别,根调用表示为0。第二列显示父调用中启动当前调用的情况。由于空间不足,我们省略了NL=NX=NB=?的调用,因为它们通过避免第6到29行的循环立即返回给父调用。</p><p class="ztext-empty-paragraph"><br/></p><p class="ztext-empty-paragraph"><br/></p><figure data-size="normal"><img src="https://pic1.zhimg.com/v2-c9d4a518012060467b6bb1b76f8ee0d3_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1404" data-rawheight="1836" data-original-token="v2-e91eac78216840f4baf66e79f558758d" class="origin_image zh-lightbox-thumb" width="1404" data-original="https://pic1.zhimg.com/v2-c9d4a518012060467b6bb1b76f8ee0d3_r.jpg?source=1940ef5c"/></figure><p class="ztext-empty-paragraph"><br/></p><p class="ztext-empty-paragraph"><br/></p><p class="ztext-empty-paragraph"><br/></p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-c11477b7a78cb031d224d5d75bd62eb1_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1022" data-rawheight="540" data-original-token="v2-10b520f5b8d5eca541b842184eea8759" class="origin_image zh-lightbox-thumb" width="1022" data-original="https://picx.zhimg.com/v2-c11477b7a78cb031d224d5d75bd62eb1_r.jpg?source=1940ef5c"/></figure><p class="ztext-empty-paragraph"><br/></p><p class="ztext-empty-paragraph"><br/></p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-4c09d9d35f8edbd45ec971968ac6b713_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="962" data-rawheight="380" data-original-token="v2-9be8b05b4aa836383b55a3c790294378" class="origin_image zh-lightbox-thumb" width="962" data-original="https://picx.zhimg.com/v2-4c09d9d35f8edbd45ec971968ac6b713_r.jpg?source=1940ef5c"/></figure><p class="ztext-empty-paragraph"><br/></p><p class="ztext-empty-paragraph"><br/></p><p class="ztext-empty-paragraph"><br/></p><p data-pid="KVtbCpwW">对于所有非循环图,MINCUTBRANCH只需考虑第2种情况。表II验证了链图的情况。最大递归深度取决于起始顶点的位置。在此例中,最大深度为3,但由于递归L={R3}和L={R4}被省略。对于图8,我们具有相同的递归深度,并且再次省略了表III中的第三个条目后使用L={R2}的递归。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="S1874Fq2">正如在表III的最后三个条目中可以看到的那样,MINCUTLAZY的递归调用具有C={R0,R3}和X={R1,R2},并且不发出任何其他ccps。不幸的是,这是一个无法轻易避免的执行开销。</p><p class="ztext-empty-paragraph"><br/></p><p class="ztext-empty-paragraph"><br/></p><figure data-size="normal"><img src="https://pic1.zhimg.com/v2-bb210df62a95ae63a2cca3e3b65dec50_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1030" data-rawheight="466" data-original-token="v2-f278dd7730923a006af7845628ec33e6" class="origin_image zh-lightbox-thumb" width="1030" data-original="https://pic1.zhimg.com/v2-bb210df62a95ae63a2cca3e3b65dec50_r.jpg?source=1940ef5c"/></figure><p class="ztext-empty-paragraph"><br/></p><p class="ztext-empty-paragraph"><br/></p><p class="ztext-empty-paragraph"><br/></p><figure data-size="normal"><img src="https://pic1.zhimg.com/v2-12bebaf2b55d8d9681c52c490058281f_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1042" data-rawheight="754" data-original-token="v2-932053e1ae514c04503c6538d5072870" class="origin_image zh-lightbox-thumb" width="1042" data-original="https://pic1.zhimg.com/v2-12bebaf2b55d8d9681c52c490058281f_r.jpg?source=1940ef5c"/></figure><p class="ztext-empty-paragraph"><br/></p><p class="ztext-empty-paragraph"><br/></p><p data-pid="jEnG0D1P">F. 分支划分的复杂性</p><p data-pid="fBoOF9Lp">我们通过O(i+r+l|Psymccp(S)|)来确定MINCUTBRANCH发出连续ccps的复杂性,其中i是第6行循环的迭代次数,r是REACHABLE的所有调用次数,l是REACHABLE第3行循环的迭代次数。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="7nM4-XNU">对于非循环图,我们知道|Psymccp(S)|=|S|-1成立。此外,没有v∈NB∪NX将被处理。因此,i=|S|-1,r=l=0,因为没有对REACHABLE的调用。因此,MINCUTBRANCH发出非循环图的ccp的复杂性为O(1)。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="h2d8W2Ti">循环查询的|Psymccp(S)|=1/2|S|(|S|-1)表示存在1/2|S|(|S|-1)个对称ccp。前|S|-1次调用会处理NL中的邻居。这种递归下降始终通过第15行进行初始化。有|S|-2次对第6行的循环进行调用,从第9行调用MINCUTBRANCH。这些调用总共处理了|S|-2k=1 k个邻居。总共处理了|S|-1+|S|-2+|S|-2k=1 k=1/2|S|(|S|-1)个邻居。REACHABLE被调用了r=|S|-2次,第3行的循环从不迭代,因此l=0。因此,每个发出的ccp的总复杂性为|S|^2+3|S|-8/|S|(|S|-1),在渐进意义上递减到1,因此复杂性为O(1)。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="ME5zqJlD">对于团查询,我们知道|Psymccp(S)|=2|S|-1-1成立。有2|S|-1个NL中的邻居被处理。此外,有2|S|-2-1个NX中的邻居被处理。因此,i=2|S|-1+2|S|-2-1=3/4|S|-1,r=2|S|-2-1,l=2|S|-2-|S|-3。为了发出所有对称ccp,复杂性为5/4|S|-|S|-5。每个发出的ccp的复杂性渐进增加到5/2。因此,团查询的复杂性为O(1)。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="AGjKpPQT">本节总结了我们的实验结果。我们首先简要介绍了实验设置。然后,我们重点关注每个ccp的枚举成本,正如我们在介绍中所讨论的那样,这是基本的(第IV-B节)。第IV-C节比较了TDMINCUTBRANCH和TDMINCUTLAZY的完整执行时间。最后,第IV-D节对这些算法与最佳自底向上连接枚举算法进行了比较。为了完整起见,它还展示了MEMOIZATIONBASIC的令人沮丧的结果,这强调了引言中讨论的挑战。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="5XCufITT">A. 实验设置</p><p data-pid="zA7QNi_o">对于所有计划生成器,无论它们是自顶向下还是自底向上工作,我们建立了共享的优化器基础设施。它包含了实例化、填充和查找记忆表、初始化和使用计划类、估计基数、计算成本以及比较计划的公共函数。因此,不同的计划生成器仅在负责枚举csg-cmp-pairs的代码部分上有所不同。由于我们忽略了修剪,所以成本计算对我们的研究来说并不重要,我们只是使用Cout来简单地总结中间结果的基数。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="F7TGxbpa">我们将预先计算的祖先、后代(MINCUTLAZY所需的)和一个顶点的邻居存储在一个大小为|V|的数组中。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="lYj07pDs">为了生成我们的工作负载,我们实现了一个通用的查询图生成器。在第一步中,它生成链式、星形、循环、团体和随机无环和有环图。对于后者,通过使用均匀分布的随机数选择两个关系的索引,随机添加边。第二步,使用具有高斯分布的随机生成器附加基数和选择性。由于我们忽略了修剪,这些数字不会影响计划生成器的搜索空间。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="rwKgdjMU">我们在评估中只包括那些所有计划生成器都能在100秒内处理的查询图。我们的工作负载由25,500个查询图组成。我们随机分布了随机循环查询的顶点和边的数量。我们在一台搭载Intel Pentium D 3.4 GHz、2 M字节二级缓存和3 G字节RAM的机器上进行了所有实验,运行openSUSE 11.0操作系统。我们使用了Intel C++编译器,并设置了O3编译器选项。</p><p class="ztext-empty-paragraph"><br/></p><p class="ztext-empty-paragraph"><br/></p><figure data-size="normal"><img src="https://pic1.zhimg.com/v2-7280e151f7bdf490d48c9470fc9d0eb3_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1040" data-rawheight="866" data-original-token="v2-18caaad5611d2c52d527f0490cc96242" class="origin_image zh-lightbox-thumb" width="1040" data-original="https://pic1.zhimg.com/v2-7280e151f7bdf490d48c9470fc9d0eb3_r.jpg?source=1940ef5c"/></figure><p class="ztext-empty-paragraph"><br/></p><p class="ztext-empty-paragraph"><br/></p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-799dfa32f902756cd6762ee1be9b133d_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1074" data-rawheight="872" data-original-token="v2-9213edcee6c95e8bc90a66435813b573" class="origin_image zh-lightbox-thumb" width="1074" data-original="https://picx.zhimg.com/v2-799dfa32f902756cd6762ee1be9b133d_r.jpg?source=1940ef5c"/></figure><p class="ztext-empty-paragraph"><br/></p><p class="ztext-empty-paragraph"><br/></p><p data-pid="BryC76Jq">B. 划分成本</p><p data-pid="qYkyOBF4">我们已经分析了链式、星形、循环和团体查询的惰性最小割划分(附录B)和分支划分(第III-F节)的复杂性。对于这两种划分策略,对于链式、星形和循环查询,每个发出的ccp的复杂性都是O(1)。但是对于团体查询,MINCUTLAZY的复杂性为O(|S|2),而MINCUTBRANCH仍然是O(1)。因此,我们测量了划分成本,并在这里讨论了团体查询的划分成本。图9显示了顶点数在横轴上,每个发出的ccp的执行时间在纵轴上。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="UViOG_i8">每个发出的ccp的成本在顶点数较小时是下降的。但是当顶点数达到五个或更多时,惰性最小割划分的成本再次增加。增加的成本是二次的。对于MINCUTBRANCH,成本在不到十个顶点时下降,之后略微增加。曲线开始时的下降是由于一些实例化开销,当顶点数较多时,与其他处理成本相比变得可以忽略不计。我们的结果支持我们的复杂性分析中证明的二次增加,但是对于这里考虑的顶点数量,这种效应相对较弱。请注意,由于我们使用内联汇编指令来最小化MINCUTLAZY所依赖的数据结构的访问成本,效应已经被削弱。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="kCpydMPT">对于MINCUTBRANCH,我们已经证明其复杂性渐近地增加到一个常数因子。我们的结果显示了一个非常微弱的增加。这是由于随着顶点数量的增加,缓存未命中的次数也增加。总之,我们的结果显示,在顶点数量较多时,团体查询的两种算法之间的性能差异会显著增加。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="dJeZ34Xc">对于其他三种图形形状,MINCUTBRANCH明显优于MINCUTLAZY,但与团体查询相比,差异没有那么大,因为这两种算法在这些场景中只有恒定的开销。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="iwPQXGd4">C. 计划生成的成本</p><p data-pid="ElzWJwEr">图10至图17显示了TDMINCUTLAZY和TDMINCUTBRANCH在某些查询形状上的性能结果。与前一节不同,我们不测量调用划分算法的一次成本,而是调用TDPLANGEN的整体成本。这包括所有调用BUILDTREE的成本以及所有调用PARTITIONi的成本。</p><p class="ztext-empty-paragraph"><br/></p><p class="ztext-empty-paragraph"><br/></p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-7ace59fc678228e7bf895117b56322f2_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1048" data-rawheight="872" data-original-token="v2-57fa4a75368e1d0bd5359b76728355bb" class="origin_image zh-lightbox-thumb" width="1048" data-original="https://picx.zhimg.com/v2-7ace59fc678228e7bf895117b56322f2_r.jpg?source=1940ef5c"/></figure><p class="ztext-empty-paragraph"><br/></p><p class="ztext-empty-paragraph"><br/></p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-371e72a83621ac89d39c68a9d165e8a3_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1042" data-rawheight="880" data-original-token="v2-12bd95779bcb504604ecb69902ba08f1" class="origin_image zh-lightbox-thumb" width="1042" data-original="https://picx.zhimg.com/v2-371e72a83621ac89d39c68a9d165e8a3_r.jpg?source=1940ef5c"/></figure><p class="ztext-empty-paragraph"><br/></p><p class="ztext-empty-paragraph"><br/></p><p data-pid="zqglQ2wm">为了减少测量误差,我们对给定输入的每个算法运行进行了平均计算。对于链式、星形、循环、团体和随机无环图(图10至图14),我们在横轴上给出顶点数,在纵轴上以对数尺度给出执行时间。我们使用线将平均执行时间连接起来。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="rbb-lrBz">由于对于随机生成的循环查询,算法的性能结果在相同的顶点数下明显偏离,我们单独展示了不同顶点数的结果。在横轴上,我们选择显示边的数量,并再次以对数尺度给出执行时间。我们不呈现精确的结果,而是通过贝塞尔曲线平滑的结果(图15至图17)。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="2oLat7a7">在所有图表中,我们还展示了TDMINCUTLAZY和TDMINCUTBRANCH的运行时间差异。由于连接成本计算的工作量对于两种算法来说是完全相同的(对于给定的查询图),两个运行时间的差异等于两种算法的划分成本的差异。因此,用归一化运行时间表示,我们将TDMINCUTLAZY的执行时间除以TDMINCUTBRANCH的执行时间。尽管归一化运行时间可以从绝对运行时间中轻松计算出来,但我们在我们的技术报告[7]中单独的图表中包含了归一化的运行时间。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="K3b50TmP">对于所有场景,我们可以看到TDMINCUTBRANCH优于TDMINCUTLAZY。由于差异曲线始终在TDMINCUTBRANCH上方,TDMINCUTLAZY的运行时间至少是TDMINCUTBRANCH运行时间的两倍。此外,仅划分成本的差异就超过了MINCUTBRANCH的所有成本。但请注意,在当今的数据库环境中,用于基数估计和计划成本计算的时间会是我们构建树实现所花费时间的几倍。然而,这不会改变我们差异曲线的形状!</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="gpD-d55h">对于链式、星形和随机无环查询,两个图形的运行时间趋于收敛,最多相差两倍。这并不奇怪,因为TDMINCUTLAZY只需要在每次调用划分算法时构建一棵双连通树。这意味着随着顶点数的增加,用于分配双连通树数据结构的固定开销变得可以忽略不计。在无环图中,星形查询具有最多的ccps,链式查询具有最少的ccps。因此,星形查询具有相同顶点数时最长的执行时间,链式查询具有最短的执行时间。随机无环图处于中间位置。归一化运行时间在所有无环查询形状中范围为2到3倍,详细结果请参见[7]。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="UT-f5EF-">就连接枚举而言,循环查询是循环查询中最简单的查询。图13中的循环查询结果类似于链式查询的图表。这是预期的,因为只有根调用的输入是循环查询,对于所有其他调用,循环都被截断为链式。循环查询在循环图的频谱的一侧,而团体查询作为最复杂的查询在另一侧。在这里,我们有最长的运行时间,因为团体查询具有最多的ccps。可以看到,两个运行时间的差异几乎与TDM INCUTLAZY的运行时间相一致。我们测量了多达16个顶点的执行时间。正如显示的,TDMINCUTLAZY的归一化运行时间增加到5。由于划分成本为O(|S|2)(也参见第IV-B节),随着顶点数的增加,归一化运行时间也会增加。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="QlXZ9mrK">从我们的实验证明,循环查询的归一化运行时间范围在两倍(循环查询)到五倍(团体查询)之间。在图15和图17中,我们展示了随机循环查询的结果。我们观察到,随着顶点数的增加,差异曲线向TDMINCUTLAZY的曲线偏移。对于这些随机图形形状,归一化运行时间范围在3到6倍之间,随关系和连接谓词的数量增加而增加。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="udtvAo9w">D. 总体比较</p><p class="ztext-empty-paragraph"><br/></p><p class="ztext-empty-paragraph"><br/></p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-4839161be053f11abf55f96064e099da_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1052" data-rawheight="918" data-original-token="v2-2beebbfce4a32768d15ffda0325f7dc2" class="origin_image zh-lightbox-thumb" width="1052" data-original="https://picx.zhimg.com/v2-4839161be053f11abf55f96064e099da_r.jpg?source=1940ef5c"/></figure><p class="ztext-empty-paragraph"><br/></p><p class="ztext-empty-paragraph"><br/></p><p class="ztext-empty-paragraph"><br/></p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-74c7865bb74044135752d0ec6b9d1c5a_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1064" data-rawheight="884" data-original-token="v2-5e986ae39ce0b04136f0b4af352f8f02" class="origin_image zh-lightbox-thumb" width="1064" data-original="https://picx.zhimg.com/v2-74c7865bb74044135752d0ec6b9d1c5a_r.jpg?source=1940ef5c"/></figure><p class="ztext-empty-paragraph"><br/></p><p class="ztext-empty-paragraph"><br/></p><figure data-size="normal"><img src="https://pica.zhimg.com/v2-650cc99d3d80012384c3dd18ca343808_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1080" data-rawheight="900" data-original-token="v2-83f70562260f6900cfd8b041f1cdaec7" class="origin_image zh-lightbox-thumb" width="1080" data-original="https://pica.zhimg.com/v2-650cc99d3d80012384c3dd18ca343808_r.jpg?source=1940ef5c"/></figure><p class="ztext-empty-paragraph"><br/></p><p class="ztext-empty-paragraph"><br/></p><figure data-size="normal"><img src="https://pica.zhimg.com/v2-fe0ec574b5c3aec622c91ee5693b64c6_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1068" data-rawheight="874" data-original-token="v2-cec4ff2632ca3c50fd7d941231586290" class="origin_image zh-lightbox-thumb" width="1068" data-original="https://pica.zhimg.com/v2-fe0ec574b5c3aec622c91ee5693b64c6_r.jpg?source=1940ef5c"/></figure><p class="ztext-empty-paragraph"><br/></p><p class="ztext-empty-paragraph"><br/></p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-7e73c75669ace3a4ddcb124f26c3ce35_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1074" data-rawheight="832" data-original-token="v2-ea0865886718a715083908decbb6f22f" class="origin_image zh-lightbox-thumb" width="1074" data-original="https://picx.zhimg.com/v2-7e73c75669ace3a4ddcb124f26c3ce35_r.jpg?source=1940ef5c"/></figure><p class="ztext-empty-paragraph"><br/></p><p data-pid="w3IX7VwA">前一节已经显示出我们的新算法明显优于TDMINCUTLAZY。但现在我们想看看它与MEMOIZATIONBASIC和Moerkotte和Neumann的DPCCP等最先进的自底向上连接枚举方法相比如何。由于空间的限制,我们没有将结果呈现为图表,而是在表IV和表V中总结了这些结果。我们将结果以相对因子的形式给出,其中我们将算法的运行时间与DPCCP的运行时间进行比较。我们给出了最小值、最大值和某种查询类型的所有结果的平均值。请注意,这些聚合值是从具有相同输入的算法的多次运行的平均值中获取的。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="ykUMK5FN">我们观察到,对于无环图和顶点数相对较低的图形来说,采用生成和测试划分策略的MEMOIZATIONBASIC的性能表现最差。对于链式查询,该算法的因子接近4700,对于无环图而言,这是一个糟糕的选择。TDMINCUTLAZY的性能相对较好,最大因子为3.2。除了星形查询,在顶点数增加时会有更多的缓存未命中,TDMINCUTBRANCH的性能甚至优于动态规划的最先进方法。我们测得的最低因子是0.66,对于随机无环查询。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="IGjLYmfr">对于随机有环查询和顶点数增加,MEMOIZATIONBASIC开始主导TDMINCUTLAZY。对于团体查询,它的性能平均快了3倍以上。对于这些类型的查询,TDMINCUTBRANCH仅次于其。对于团体查询的平均值为1.09,随机有环查询的平均值为1.13,这些都是非常有竞争力的结果,但因子范围也可以达到1.47,这仍然是一个非常令人印象深刻的结果,与TDMINCUTLAZY和MEMOIZATIONBASIC相比。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="m1-Jo_25">根据我们的实证分析,我们建议将TDMINCUTBRANCH作为自底向上处理的首选算法,因为它是无环图中最快的算法,并且对于随机循环图的表现非常好。我们可以将最差的相对因子1.47与1/0.66=1.52的因子进行对比,后者比动态规划的最先进方法更快,尽管没有采取剪枝策略。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="QhLCENrX">我们提出了一种新的自顶向下连接枚举算法,相比目前已知的最佳自底向上算法,它具有两个优势:</p><p data-pid="o2-xfIcT">它的性能更好。</p><p data-pid="e3ZmE7db">它更容易实现。</p><p data-pid="esWnGJu7">后者是因为它不需要复杂的数据结构,如双连接树。相反,它仅依赖于集合操作,可以使用位向量轻松高效地实现。</p><p data-pid="Uz5EcwDU">此外,这种新算法表现出与最佳自底向上算法相近的性能。重要的是,它在不依赖剪枝的情况下实现了这一点。因此,一旦查询适合进行分支限界剪枝,我们的新的自顶向下算法将优于最佳自底向上算法。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="PL2AbSLk">未来的工作面临两个主要挑战。第一个是将我们的新算法扩展到超图。这很重要,因为并非所有查询都具有等价的查询图,有些需要超图。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="J23t2ehT">目前,剪枝条件的有效性尚不清楚,即在哪种查询和剪枝程度下可以实现剪枝。因此,第二个更具挑战性的问题是确定这些条件。</p><p></p>
<p data-pid="Q0abvHs3"><b>导读</b>所有使用数据库的同学都非常关心数据库对 SQL 有什么样的优化,这直接决定着查询的性能。这也是我们在优化器上持续不断地投入研发的根本原因。本文将分享 Doris 新优化器开发过程中的一些感悟。</p><p data-pid="6KDqDfD8"><b>主要包括以下四大部分:</b></p><p data-pid="E5YW4Iim">1. 重塑</p><p data-pid="WYus64jS">2. 优化的本质</p><p data-pid="Z1q0dtvF">3. 性能瓶颈</p><p data-pid="aW8F7I00">4. 挑战分享嘉宾|周明宏 博士 SelectDB 高级研发工程师</p><p data-pid="jSYMyxyG">编辑整理|小宁</p><p data-pid="VdrTb6R7">内容校对|李瑶</p><p data-pid="6man94Ho">出品社区|DataFun</p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-7c2f1887b15fe072d35550bb51dde1c0_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1080" data-rawheight="544" data-original-token="v2-7c2f1887b15fe072d35550bb51dde1c0" class="origin_image zh-lightbox-thumb" width="1080" data-original="https://picx.zhimg.com/v2-7c2f1887b15fe072d35550bb51dde1c0_r.jpg?source=1940ef5c"/></figure><p data-pid="YqVLyncd">过去一年是 Doris 商业化的元年,遇到了很多新的场景和挑战。在这些挑战中,我们发现老优化器有很多问题。首先是缺少优化规则的抽象。有些规则并不适用于所有的场景,对某些场景,有些规则有帮助,有些规则没有帮助。因为缺少规则的抽象,不方便细粒度控制规则的使用,给 query 的调优带来很大麻烦。同时,为了适应更多的数据场景,需要增加新的规则。添加规则的成本,除了实现规则本身,还要让规则融入优化器,这就带来很多额外的成本。最后,老优化器不方便我们观察一个规则究竟给 plan 带来了怎样的变化。</p><p data-pid="kXnXEgvk">除了优化规则的抽象,还有一个更重要的问题是我们缺少 CBO 的框架。老优化器缺少统计信息收集的能力。代价模型代码也零散的分布在整个优化器中。所以只能做一些很简单很有限的 CBO 的规则。如果拿到复杂的 query,基本只会生成一个左深树。</p><p data-pid="fSFcH6uN">最后优化器本身的架构,基本只是做树的两轮遍历。这对优化规则的有很大限制。</p><p data-pid="P-yMM18c">于是在去年,Doris 决定做一个新的优化器。从此拉开了新优化器的序幕。</p><figure data-size="normal"><img src="https://pic1.zhimg.com/v2-bf260231e396e280fa5c02124da474f3_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1080" data-rawheight="490" data-original-token="v2-bf260231e396e280fa5c02124da474f3" class="origin_image zh-lightbox-thumb" width="1080" data-original="https://pic1.zhimg.com/v2-bf260231e396e280fa5c02124da474f3_r.jpg?source=1940ef5c"/></figure><p data-pid="msgeWDJ-">经过来自美团、百度、小米、腾讯、京东、SelectDB 等公司的工程师们 400 多天的努力工作后,我们的优化器终于与大家见面了,我们会在 Doris2.0 的时候正式推出。在前面的研发过程当中,我们也在不断做测试。比如在 SSB、TPCH 500G/1T 这些测试中,我们都超过了由专家人工改写的 SQL。也就是说,即使是经验丰富的 DBA 用老优化器改写的 SQL 仍然比新优化器自动生成的 plan 要差一些。图中是我们测试的对比,大多达到了 50% 以上的提升。同时为了检验泛化能力,在用户 POC 测试中,超过 10 个测试使用了新优化器,在这些测试中,新优化器为用户减轻了很多负担,SQL的调优的工作几乎都是自动完成。</p><p data-pid="rxC7uKBk">下面从 SQL 的本质来讲解一下优化器的地位。</p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-03ce5407343420df1b5a226461cb3d1c_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1080" data-rawheight="467" data-original-token="v2-03ce5407343420df1b5a226461cb3d1c" class="origin_image zh-lightbox-thumb" width="1080" data-original="https://picx.zhimg.com/v2-03ce5407343420df1b5a226461cb3d1c_r.jpg?source=1940ef5c"/></figure><p data-pid="q71evIwi">首先,SQL 的本质是一个描述性的语言,用户只需要描述需要的数据是什么,数据怎么拿到是由优化器决定的。可以说执行引擎是车的发动机,优化器是车的方向盘,如果没有一个正确的优化器,越强劲的引擎只会让我们以越快的速度撞到内存溢出的南墙上面。所以优化器是 SQL 中最有特色的部分。</p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-45a09a1f003cceefc929e5ca53e405eb_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1080" data-rawheight="607" data-original-token="v2-45a09a1f003cceefc929e5ca53e405eb" class="origin_image zh-lightbox-thumb" width="1080" data-original="https://picx.zhimg.com/v2-45a09a1f003cceefc929e5ca53e405eb_r.jpg?source=1940ef5c"/></figure><p data-pid="VGcbh48d">优化器在一条 SQL 的旅程中处于什么位置呢?如上图中所示,它处于红框范围内。一条 SQL 首先经过语法解析,生成抽象语法树,然后进行语义分析后,第一阶段对逻辑计划进行改写,改写中会引入很多规则。改写完成后,会生成多个候选的 plan,通过代价模型估计,从中挑出一个最优的执行计划,交给查询引擎执行。Nereids 优化器主要是指改写查询计划、优化查询计划两部分。</p><figure data-size="normal"><img src="https://pica.zhimg.com/v2-7058d818b9912b9debfc22ebb1045847_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1080" data-rawheight="607" data-original-token="v2-7058d818b9912b9debfc22ebb1045847" class="origin_image zh-lightbox-thumb" width="1080" data-original="https://pica.zhimg.com/v2-7058d818b9912b9debfc22ebb1045847_r.jpg?source=1940ef5c"/></figure><p data-pid="I8YK7xr1">具体来说,改写的部分称之为 RBO,代价估算称之为 CBO,为了和之前的优化器、执行引擎兼容,还需要“plan 翻译”才可以把新优化器生成的 plan 转换成 BE端可理解的查询计划。</p><p data-pid="Fnqkoc24">在 RBO 规则中,包括了:谓词下推、表达式改写、常量折叠、消除空算子等等。这些规则一般来说会提高查询的效率。所以这些规则生成的 plan 会替代输入的 plan。有时一组规则会反复使用,知道 plan 不再变化为止。</p><p data-pid="jlvhiWTN">当 RBO 改写完成后,生成的 plan 作为 CBO 阶段的输入。CBO 阶段最主要的任务是决定 join 的顺序。因为 join 的顺序对 plan 的影响是非常大的。除了 join 顺序的选择,还有许多需要代价估算的点:包括 CTE,即一些 with 语句生成的 view,是单独算出来还是嵌入到 SQL 中;还有聚合选用哪一种策略,是一阶段还是两阶段、三阶段做甚至四阶段做。这些都是 CBO 阶段做的事情。</p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-a3a6d6fd9675d3ef7e9e5425bb324f4a_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1080" data-rawheight="607" data-original-token="v2-a3a6d6fd9675d3ef7e9e5425bb324f4a" class="origin_image zh-lightbox-thumb" width="1080" data-original="https://picx.zhimg.com/v2-a3a6d6fd9675d3ef7e9e5425bb324f4a_r.jpg?source=1940ef5c"/></figure><p data-pid="a0Inl7gG">上面讲了很多优化规则,其核心思想就是让 SQL 执行更快。但是就像做科学研究,定义好一个问题比解决问题更有价值。怎么叫定义好一个问题?就是需要找到一个好的指标来衡量这个问题。如果我们用查询的时间来衡量,这是一个正确但是无用的指标。所以根据我的理解,应该将这个问题定义为:尽早降低数据规模。下面我们从这个角度来审视优化的规则。</p><figure data-size="normal"><img src="https://pic1.zhimg.com/v2-b9b6795225cdcd19831dd15a1161eacd_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1080" data-rawheight="607" data-original-token="v2-b9b6795225cdcd19831dd15a1161eacd" class="origin_image zh-lightbox-thumb" width="1080" data-original="https://pic1.zhimg.com/v2-b9b6795225cdcd19831dd15a1161eacd_r.jpg?source=1940ef5c"/></figure><p data-pid="7OHqCKGL">用一个简单的例子来看。上面是一个 TPC-H 的例子,我们做了改写。描述的是有很多订单,订单的买方和卖方都有国籍,我们需要选出中国和美国之间贸易往来的订单,生成下面的表格。</p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-051ef8f085d3f390da8ecd4d9421796c_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1080" data-rawheight="607" data-original-token="v2-051ef8f085d3f390da8ecd4d9421796c" class="origin_image zh-lightbox-thumb" width="1080" data-original="https://picx.zhimg.com/v2-051ef8f085d3f390da8ecd4d9421796c_r.jpg?source=1940ef5c"/></figure><p data-pid="SY-zMJ19">理解了这个 SQL 的做法,我们就能够理解优化器在其中做了什么事情。根据条件:(卖方的国籍=中国,并且买方的国籍=美国)或者(卖方的国籍=美国,并且买方的国籍=中国)表示中美之间往来的订单,这个条件无法拆开,一个最自然的做法是把 orders 表和 customer表先 join,然后和 supplier 表 join,再和 nation join,最后做过滤。这是第一种做法,效率比较低。</p><p data-pid="iLYPJYrf">好一些的做法是,既然挑出的是中美之间的贸易,那么可以推导出一个条件:customer 只选出中国或美国的 customer,supplier 只选出中国或美国的s upplier,这就是我们优化器所做的一个优化,我们会推导出一些看似冗余的条件,但这些条件作用非常大,可以帮助我们尽早将 customer 和 supplier 的数据规模降低。数据规模降低再来与 orders 表做 join 时,右表的数量就不再是全体 customer,而只是 customer 的一部分。再和 supplier 做 join 时,右表的数量也只是一部分的 supplier,左表的数量也不是全体的 order,而只是 customer 是中国或美国的 order,所以对这个 join 来说,也降低了数据量。这两个 join 在 TPC-H 查询中的性能提高是非常显著的,有 2-3 倍的提升。这里的核心思想就是尽早把数据规模降低。希望这个例子能帮大家理解我们的优化器殚精竭虑想要达到的目的。</p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-69fdb9e3f8537647df9226b8710e6cc2_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1080" data-rawheight="519" data-original-token="v2-69fdb9e3f8537647df9226b8710e6cc2" class="origin_image zh-lightbox-thumb" width="1080" data-original="https://picx.zhimg.com/v2-69fdb9e3f8537647df9226b8710e6cc2_r.jpg?source=1940ef5c"/></figure><p data-pid="DkRnQPUc">除了上面的方法,还有一个更重要的降低数据规模的途径是:调整 join 的顺序。在事实表和维度表做 join 的时候,往往维度表会有一些过滤条件,会对事实表有很强的过滤效果。但是这时候又面临一个问题:我们知道 join reorder 是一个 NP的问题,当表的数量增加时,候选 plan 会呈几何级数增长。这么多年来,这个问题没有特别创新的突破,NP 问题一般只能通过动态规划的方法,找一些次优的解。我们现在看到的所有方法,都是动态规划不同的应用,比如有:DPSize、DPSub、DPhyper、Cascading......我们的新优化器 Nereids 上面,同时采用了两种动态规划的方法,基础的有 Cascading 方法,同时也加上了 DPhyper 的方法,两者相互补充。</p><p data-pid="oxBL4g3d">艰苦奋战了大半年后,系统基本成型。接着就开始关注性能。</p><figure data-size="normal"><img src="https://pic1.zhimg.com/v2-a0f13435896d6d11db4717c36fe4ff47_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1080" data-rawheight="607" data-original-token="v2-a0f13435896d6d11db4717c36fe4ff47" class="origin_image zh-lightbox-thumb" width="1080" data-original="https://pic1.zhimg.com/v2-a0f13435896d6d11db4717c36fe4ff47_r.jpg?source=1940ef5c"/></figure><p data-pid="zbMSLJ_G">第一个很重要的性能提升来源于 RBO 阶段的一次重要重构。Cascading 框架有一个 Memo 结构,相当于一个小账本,所有规则产生的 plan 都用 Memo 记下来。这是CBO阶段执行动态规划算法所需要的。我们知道所有动态规划方法都有小账本,在 Cascading 中的小账本就叫做 Memo。我们一开始就像论文中的流程一样,将 plan 放在 Memo 中,对plan进行改造时再从 Memo 中把 plan 片段取出来,这个过程叫 copyIn,copyOut。RBO 阶段,总是用一个新的 plan 来替代老的 plan,我们仍然遵循老的套路,每次生成新的plan,就放入 Memo 中,进行下一个规则替换时,再将这个 plan 从 Memo 中拿出来。反复 copyIn,copyOut 的动作其实是多余的。于是美团的华建老师闭关好几周,给大家提交了一个巨大的pr,RBO 的效率得到了飞速的提升。当然我们也很痛苦,因为需要重新看一遍RBO 的所有代码。但我们非常欢迎有越来越多这样的痛苦。</p><p data-pid="sjrr0m-s">RBO 阶段有了一个性能的飞跃之后,CBO 阶段的性能问题就凸显出来了。于是我们团队里的 ACM 冠军,莫琛辉同学出场了。在那几个星期里,他分析了上百个火焰图,改写了好几轮代码,最终将很多秒级的分析时间压缩到 20 毫秒左右。如果将来你也需要实现一个 cascade 矿建,那么 CostAndEnforce 部分的性能调优一定值得深入挖掘。</p><p data-pid="In06X69G">下面再介绍下开发过程中所面临的各种各样的挑战。这部分可能是介绍中最有意义的部分。</p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-d1f91d85a02e4a325ff30adffb7a6920_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1080" data-rawheight="607" data-original-token="v2-d1f91d85a02e4a325ff30adffb7a6920" class="origin_image zh-lightbox-thumb" width="1080" data-original="https://picx.zhimg.com/v2-d1f91d85a02e4a325ff30adffb7a6920_r.jpg?source=1940ef5c"/></figure><p data-pid="Mm9jQ1BV">第一个问题就是公平与效率的平衡。这个问题的本质是,做 join reorder 时候,希望找出做好的 plan,但是这是个 NP 问题,这就意味着我们一定要做裁剪,不可能公平地给每个可能的 plan 检验的机会,有一些plan未经检验就直接被淘汰掉了。最简单的树结构是 Left Deep Tree。这种树,一般大表放在最左边,隐含了一个假设:我们现在做的都是 hash join,用右表 build hash table, 左表做probe。因为一行数据构建hash table的成本远远高于探测 hash table 的成本,所以要把成本高的计算放在行数少的表上做,单行成本低的计算放在行数大的表上做。这就是最基本的思想。所以构造出一个左深树,把最大的表(事实表)放在最左边,依次把小表(维度表)放在右边,和大表 join。这个方法的优点是搜索空间小(比后面两种小很多),因此优化器的执行就会快。但是这样往往会错过很多优秀的执行计划,让引擎端的负担太大。一个更好的平衡是 ZigZag Tree。它背后的思想是:当大表和小表 join 后,得到的结果可能数据量比较小,再和另外的表join 时,可能就需要放在右边,于是生成了中间所示的执行计划,每一步都去需要判断左表和右表谁大谁小,就得到了 ZigZag Tree。如果将搜索空间再扩大一点,称之为 Bushy Tree,就是把所有的二叉树都放进来考虑,Bushy Tree 的搜索空间因此会暴涨上去。当表的数量太多时,就会导致优化器执行时间超过了执行引擎的执行时间。所以很多情况下,不能搜索整个空间的 Bushy Tree。</p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-5a0d09692f1de7c22225bfdb83b9f548_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1080" data-rawheight="530" data-original-token="v2-5a0d09692f1de7c22225bfdb83b9f548" class="origin_image zh-lightbox-thumb" width="1080" data-original="https://picx.zhimg.com/v2-5a0d09692f1de7c22225bfdb83b9f548_r.jpg?source=1940ef5c"/></figure><p data-pid="5vJlcE8A">这里有一个基于表数量和 Left Deep Tree、ZigZag Tree、Bushy Tree 增长幅度的估算。在实际实践当中,当表的数量较少时,会采用 Cascading,优势是可以把除了 join reorder 外的各种 rule 和 join reorder 混合使用。但是效率不是太高,所以当表的数量较多时,会切换到 DPhyper 上去。</p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-506f5edea18d4501368aaa7d1ba59947_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1080" data-rawheight="498" data-original-token="v2-506f5edea18d4501368aaa7d1ba59947" class="origin_image zh-lightbox-thumb" width="1080" data-original="https://picx.zhimg.com/v2-506f5edea18d4501368aaa7d1ba59947_r.jpg?source=1940ef5c"/></figure><p data-pid="VE8zcwZ6">第二个挑战是我们始终与误差共存。与误差共存不代表我们不去努力消除误差。先来看看误差是怎么产生的。刚才说要做 join reorder,这步需要很多的统计信息,需要估算每次 join 的结果有多少行,经过过滤有多少行等。所以第一个误差是统计信息,误差来源是抽样。比如计算某个字段不同值的个数(distinct),称之为NDV(number of distinct values),不可能全量做真实计算,一般会使用抽样的方式,就会带来误差。</p><p data-pid="vCun_CIK">对表里数据统计后开始计算,比如一张表里有学生信息,有过滤条件:选出其中男同学数量。假设表有 100 行,优化器估计选中男同学数量为 50%,但如果表来自国防科大,可能选出男同学数量占 98%,就会导致统计信息的推导出现误差。在这些例子中,误差产生的主要原因一是统计信息有误差,二是加入了一些假设,比如假设数据均匀分布,假设字段之间没有相关性等。所以统计信息推导时候,在统计信息误差基础上,又加入了一些误差。这些误差中,有一些是需要努力消除的,有一些是特殊应用场景引入的。</p><p data-pid="2V3haZmO">如何检验统计信息的推导是否准确呢?我们开发了一个工具叫 qError,它会把每个算子推导的行数和实际执行的行数做比较计算,来检验推导是否准确。当我们把推导信息也做出来后,就开始计算各个 plan 真实的代价,这一步称为代价模型。这部分需要考虑引擎的特点,环境的差异等,要看更需要减少数据在网络的传输?还是更看重机器内存的代价,或是 CPU 的代价等。不同情况要做不同的权衡。所以代价模型在统计信息推导误差的基础上,又会有新的误差。如何衡量代价模型呢?我们推出了 Plan Ranker 工具。不管是 Cascading 还是 DPhyper,都是动态规划方法,我们都加入了 Memo 记录不同的 plan。我们可以取出认为排名前十的 plan,实际执行中,他们的效率是否和我们预期的排序是一致的呢,可以通过实际执行来检验。将检验后的 plan 序列与推断的 plan 序列进行距离计算,来衡量代价模型的好坏。</p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-62b1dfb89f4a4700b9b6ed8f947035ae_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1080" data-rawheight="607" data-original-token="v2-62b1dfb89f4a4700b9b6ed8f947035ae" class="origin_image zh-lightbox-thumb" width="1080" data-original="https://picx.zhimg.com/v2-62b1dfb89f4a4700b9b6ed8f947035ae_r.jpg?source=1940ef5c"/></figure><p data-pid="zYH-I_jn">最后,还有一个“颠覆者”。它的出现颠覆了我们对 join reorder 以及做优化时的很多认识。先用一个简单的例子来理解下 Runtime Filter,它对我们做 join 时有非常大的影响。假设有一个订单表,有订单号、商品id和其他字段。还有一个商品表,有商品 id、品牌,一个品牌下有多个商品。现在要找出“华为”这个品牌下的订单。首先对商品表做过滤,找出品牌“华为”的商品,然后和订单表做 join。刚才我们说过,优化的目的是要尽早降低数据规模,那么这时候会有个想法。</p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-01fdfde5417c4aa6737bb0bef8dfe3a7_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1080" data-rawheight="607" data-original-token="v2-01fdfde5417c4aa6737bb0bef8dfe3a7" class="origin_image zh-lightbox-thumb" width="1080" data-original="https://picx.zhimg.com/v2-01fdfde5417c4aa6737bb0bef8dfe3a7_r.jpg?source=1940ef5c"/></figure><p data-pid="yH4A-f1O">假设商品表过滤出“华为”品牌的商品 id 是 p001、p003,作为集合 A,是否可以把A发送给订单表,先用商品 id 对订单集合做一次过滤,过滤后 6 亿条数据只剩2400 万条做j oin。这个是来自 TPC-H 里的典型场景,数据比例也是这样。这样就可以大大降低最后一步 join 的负担,提高整个查询的效率。本来 Runtime Filter 一开始的出现被认为是额外的 bonus,如果优化器里的规则是一等公民的话,它就是二等公民,可是这个二等公民颠覆了我们的想法。</p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-f8d5d93c1bfe316971a8db7406634675_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1080" data-rawheight="607" data-original-token="v2-f8d5d93c1bfe316971a8db7406634675" class="origin_image zh-lightbox-thumb" width="1080" data-original="https://picx.zhimg.com/v2-f8d5d93c1bfe316971a8db7406634675_r.jpg?source=1940ef5c"/></figure><p data-pid="6CWmvIps">换一个稍微复杂的例子。假设需要找出亚洲的 supplier,supplier 有 nation id,nation id 有 region id(这里只选出亚洲)。supplier 先和 nation join,再和 region join。在 TPCH 里,region 有 5 大洲,nation 表有 25 个国家,每个洲5 个国家。每个国家有一些供应商,supplier 表有 1 千万条数据,并且每个国家的 supplier 是均匀分布的。左册的破烂相对右侧 plan,就不够高效。右边首先选出亚洲,和 nation 做 join,这样只选出亚洲的 5 个国家,再和 supplier 做 join,就直接选出了亚洲国家的 2 百万 supplier。可以看到,左表和右边都有两个 join,但是处理的数据量级是不一样的。显然右边处理的数据量级小了很多。因为 1 千万的数据 join,右边只做了一次,而左边做了两次。传统观点下,右边plan 是远远优于左边 plan 的。下面就会看到为什么将 Runtime Filter 称为颠覆者。</p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-abf5d8c19bf5748e0a2f2f1880497291_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1080" data-rawheight="607" data-original-token="v2-abf5d8c19bf5748e0a2f2f1880497291" class="origin_image zh-lightbox-thumb" width="1080" data-original="https://picx.zhimg.com/v2-abf5d8c19bf5748e0a2f2f1880497291_r.jpg?source=1940ef5c"/></figure><p data-pid="L3VN5S4L">颠覆效果是这样的。左边是我们刚才认为优秀的 plan,右边是认为不优秀的plan。但是加上 Runtime Filter 后,右边因为 region 只选择亚洲,所以会把亚洲的 region id 发送给 nation,于是 nation 表在扫描时候只会取出 5 条数据,因为 nation 表通过 Runtime Filter 做了过滤。Nation 表过滤后,会生成下一个Runtime Filter,把5个国家的 id 发送给 supplier,于是 supplier 表直接过滤出2 百万数据出来。如果采用右边的 plan,参与 join 的数据规模就没有出现过1千万。这样,右边执行反而更占优势。而且可以看到,除了 join 以外,它对延迟物化也非常有帮助。在 Doris 存储层里,除了要取 key 字段外,还要取很多其他字段。Doris是 列存数据,当把 nation 表过滤出的 5 个 id 发送给 supplier 以后,supplier 上其他字段的访问数据量也会减少,我们把这称为延迟物化。像左边这样,supplier 其他字段都需要读取出来。而在右边情况下,可以通过 index,只需要点查取出相关的行。所以,有了 Runtime Filter 加持后,右边的 plan 反而比左边更有效了。但是 Runtime Filter 又有不确定性,因为无法确定 Runtime Filter 有多高的过滤率,这个依赖统计推导。同时,可能因为 Runtime Filter 等待时间过长,导致整个查询时间变长。如果要实现刚才那种理想的运行效果,supplier 扫描必须要等到 region 扫描完成,nation 扫描完成,才能得到有效的Runtime Filter。假设 region 扫描变慢了,nation 没有等到 region 的扫描结果,直接生成 Runtime Filter 交给 supplier,其实没有任何过滤效果。所以Runtime Filter 的过滤效果比较动态,这给查询优化带来非常大的挑战。这也是我们下一步要去解决的重要问题。</p><p class="ztext-empty-paragraph"><br/></p><figure data-size="normal"><img src="https://pica.zhimg.com/v2-4271cef95b6f9967fd2ffdbad5761a54_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1080" data-rawheight="490" data-original-token="v2-4271cef95b6f9967fd2ffdbad5761a54" class="origin_image zh-lightbox-thumb" width="1080" data-original="https://pica.zhimg.com/v2-4271cef95b6f9967fd2ffdbad5761a54_r.jpg?source=1940ef5c"/></figure><p data-pid="6RUhUoIs">Doris2.0 即将推出,Nereids(待确认)新优化器也会随之推出,欢迎大家试用。Doris 社区也非常欢迎大家扫码加入。最后欢迎大家关注 SelectDB。</p><p data-pid="c5vrUSsU"><b>1、CostAndEnforce 在优化器优化的思路是什么?</b></p><p data-pid="jNojnVFN">打出火焰图,找出热点,分析热点部分有没有做重复计算,比如有没有做重复的统计信息推导,有没有重复计算 cost。通过火焰图,可以得到一些线索,更快找到从哪里分析出现的重复计算。</p><p data-pid="o7QU1DAm"><b>2、当过滤条件很多,或者非等值的条件下,Runtime Filter 效率是否会下降很多?</b></p><p data-pid="ndbJzQlD">不会。如果对右表有越多的过滤条件,Runtime Filter 效率会越高,因为对左表的过滤性会更强。如果没有等值条件,不会生成 runtimeFilter</p><p data-pid="BQeZySPs"><b>3、Runtime Filter 对 left join 是否有优化效果?</b></p><p data-pid="bezkVggs">对 left outer join 没有优化效果。因为不能在左表的扫描端把数据过滤掉,因为左表不管能否跟右表匹配,都需要把数据输出。所以 left outer join 不能运用Runtime Filter。</p><p data-pid="fTvjTT35"><b>4、Doris 支持分页查询么?</b></p><p data-pid="ZZgFM5oH">分页查询支持。</p><p data-pid="OBqMo7Qy"><b>5、左表等 Runtime Filter 要等多久?</b></p><p data-pid="gQK-Vpal">这是经验参数,我们默认等 1 秒。</p><p data-pid="JbU9hv2X"><b>6、优化器以后可以交给 AI 吗?</b></p><p data-pid="yAI-mv4s">DB for AI 是一个新的研究方向。几十年来,优化器我觉得没有本质的进展,都是拿着同一件武器——动态规划,只是打的不同的拳法。可能 AI 是一个新的武器,但是目前还没有看到特别的效果,特别是 ad-hoc 查询中。</p><p data-pid="DW6bBpRa"><b>以上就是本次分享的内容,谢谢大家。</b></p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-87a3b4de9237b0b284d0cbebe2403914_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="681" data-rawheight="347" data-original-token="v2-87a3b4de9237b0b284d0cbebe2403914" class="origin_image zh-lightbox-thumb" width="681" data-original="https://picx.zhimg.com/v2-87a3b4de9237b0b284d0cbebe2403914_r.jpg?source=1940ef5c"/></figure><p></p>
<p data-pid="dFxmxXca">导师让下周给他讲四篇paper,感恩节也没法休息咯。</p><hr/><p data-pid="aJpWxQR4">导师主要是做ML for DB的方向,所以这几篇paper也是关于这个方面的,更具体来讲是强化学习在优化器和查询调度上的应用。这次的paper带来的是Bao,一个具备高度可落地性的强化学习查询优化组件。在此之前其实已经有很多关于在优化器中应用机器学习的先例,比如Neo。这些工作虽然在一定程度上解决了问题,但是在工程落地的方面都或多或少存在不足。</p><ul><li data-pid="0Kh62_Qi">冷启动的问题。由于之前的很多工作是对原有的优化器进行了替换,所以所有的统计信息都需要重新开始收集。这就导致了需要大量的初始化训练数据来构建一个可用的模型。</li><li data-pid="x2ddSlvt">无法根据动态变化的workload和数据进行调整。之前基于监督学习的优化器对于数据和workload的动态调整没有办法做到快速响应,因为每一次的数据和workload变更都会导致一次昂贵的重训练。</li><li data-pid="08Ijd9Sy">尾延时过高。之前的工作更多的是在优化平均延时,但是对于尾延时来说甚至比原有的性能慢两个数量级。</li><li data-pid="cGoDCttV">可解释性不足带来的黑盒问题。对于DBA和用户来说,这些基于机器学习的优化器非常难以理解,他们也无法搞清楚它是如何做出的最终决策。</li><li data-pid="HIcFULUk">实际部署上的成本过大。所有现有的类似系统都还处在research原型阶段,距离生产部署还有很长的路要走。</li></ul><p data-pid="93m7PX93">不同于之前的工作,BAO的目标不是取代DBMS原有的优化器,而是在它的基础上进行构建。具体来说,BAO会维护一组优化提示集合。对于每一个集合,BAO会让优化器生成一个特定的执行计划,然后利用BAO的RL推理框架选择其中一个下发到DBMS的执行引擎。</p><p data-pid="qnH9_TGi">这一套方案对上述的每一条不足都进行了针对性的解决。</p><ul><li data-pid="lSb8BLMB">更少的启动时间。这个很好理解,因为Bao并没有取代原有的优化器,而是在其之上进行工作,所以可以直接复用优化器使用的全局信息。</li><li data-pid="tMDIdiUT">带来更低的尾延时。关于这一点下文有详细的描述。</li><li data-pid="4nsJ3jFs">更好的可视化与可解释性。Bao提供一整套决策可视化的接口以供调用方查看它具体的决策流程。与此同时,Bao还可以以query为粒度开启或关闭。</li><li data-pid="RWgmOcMq">更低的接入成本。由于Bao的设计,它在与db结合的时候往往只需要获取相应的hint以及调用hook即可。由于这些信息通常已经被db通过接口的形式暴露在外,所以大部分时候可以做到无侵入的接入Bao。</li><li data-pid="BbbIkQR_">更好的扩展性。Bao可以很轻松的添加额外的优化提示,例如可以在向量的最后加上有关机器缓存的状态。Bao在学习到这一个hint之后可能会做出更理性的决策。这一点在看完下文关于Bao对于查询计划的向量化表示就可以更好的理解。</li></ul><figure data-size="normal"><img src="https://pica.zhimg.com/v2-c596200bcb30f83d939e51b4075f0dc4_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="515" data-rawheight="261" data-original-token="v2-1afdd5dd23f38df569ab010011a54192" class="origin_image zh-lightbox-thumb" width="515" data-original="https://pica.zhimg.com/v2-c596200bcb30f83d939e51b4075f0dc4_r.jpg?source=1940ef5c"/></figure><p data-pid="zP9sGnnG">Bao的架构可以参考上图。具体来说,它是由一个多层的Tree-CNN组成。这个Tree-CNN的设计以及它是如何结合汤普森采样的我们放到下一段再讲。</p><p data-pid="XspYYdg-">再来看看Bao的工作流程。Bao会维护一组优化提示集合,每一个集合包含各种优化开关,例如走不走索引,用不用hash join等等。当一个查询下发之后,Bao会为每一个优化提示集合生成一个查询计划,并对这个查询计划的执行时间进行估算。获得估算的指标之后,需要用到汤普森采样的方法来选择合适的执行计划下发到执行引擎。至于在这一步为什么不直接选择,这涉及到其中的统计学问题。</p><p data-pid="RKzwTLm-">简单来说,如果我们一味的选择best performance的执行计划,那么由于我们此时获得的执行时间也是估计的,并且有可能并不准确,这会让我们一直陷入一个错误的选择当中。此外,如果我们一直不去尝试其他的选择,那么就永远不会从他们当中学习到经验。这是一个exploration与exploitation的取舍问题,也是Bao在实际场景中面临的统计学抽象问题CMAB的核心之一。这一部分在下文Bao的学习过程中会有简单的描述。</p><p data-pid="2EV71mr9">执行结束之后,Bao会将真正执行的时间以及其他性能指标加入Bao的经验集中,为下一次重新训练做准备。至此,一次完整的query就结束了。</p><p data-pid="bGsMxNak">这一块可能会涉及一些RL和统计学知识,我对这方面也只是略懂皮毛。想了解更多的话推荐去看 <a class="member_mention" href="https://www.zhihu.com/people/866e63341ae3873b7a4ce0390767dc74" data-hash="866e63341ae3873b7a4ce0390767dc74" data-hovercard="p$b$866e63341ae3873b7a4ce0390767dc74">@覃含章</a> 大佬的回答,讲的很牛逼。简单来说,Bao面临的问题可以被抽象的定义为一个contextual multi-arm bandit问题(CMAB)。在CMAB中,优化的目标是尽可能地最小化regret(例如我们可以定义regret为,最优优化提示产生的I/O次数与实际执行产生的I/O次数的差值就可以被理解为regret)。通过使用比较经典的汤普森采样方法可以解决这一类的问题。</p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-07df16ad4a5b2f08220526583e9c5a23_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="514" data-rawheight="216" data-original-token="v2-c879c021e169e0506ed830b7c81195a6" class="origin_image zh-lightbox-thumb" width="514" data-original="https://picx.zhimg.com/v2-07df16ad4a5b2f08220526583e9c5a23_r.jpg?source=1940ef5c"/></figure><p data-pid="Ulyy1PLn">预测模型是Bao应用汤普森采样的核心部分,它在这里使用了Tree-CNN(TCNN)来做预测。在这一部分我们主要关注如何将一个查询计划转换成树形向量来作为TCNN的输入,以及如何将TCNN与汤普森采样结合。</p><p data-pid="Z-plkIbT"><b>构建输入</b></p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-9bc05672a794bdde964bd881987b5191_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="766" data-rawheight="432" data-original-token="v2-257bc37061a75901fedcd1b77d311b0c" class="origin_image zh-lightbox-thumb" width="766" data-original="https://picx.zhimg.com/v2-9bc05672a794bdde964bd881987b5191_r.jpg?source=1940ef5c"/></figure><p data-pid="2aHp2YPe">这一部分看图理解更容易一些。首先将查询计划树进行二值化(如上图所示)之后,Bao将每一个查询算子的类型进行one-hot编码,并在后面加上相应的cost和cardinality组成了一组树形向量(如下图所示)。在每个向量的后面还可以根据需求添加相应的统计信息,例如当前的系统cache状态等等。</p><figure data-size="normal"><img src="https://picx.zhimg.com/v2-5ee8d1b8f1df0691db6dae003178827e_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="790" data-rawheight="344" data-original-token="v2-f04bd932fe88ba19e95a699c9d044cbb" class="origin_image zh-lightbox-thumb" width="790" data-original="https://picx.zhimg.com/v2-5ee8d1b8f1df0691db6dae003178827e_r.jpg?source=1940ef5c"/></figure><p data-pid="kVPlr87m">TCNN包括一组构建于查询计划树上的树形过滤器。通过这一组过滤器可以构建一个变换之后的新树。过滤器的作用在于层级查找查询计划树上的规律,例如一组hash join,一个不必要的索引查询等等。越靠后的过滤器越可以发掘到更复杂的规律,例如一个很长的链式merge join等。在过滤器之后是一个用来扁平化向量的动态池化层,以及两个用来将池化的向量map到最终预测值的全连接层。</p><p data-pid="0jSMwHhJ">由于汤普森采样要求从给定的经验集中进行采样,最简单的一种方式就是从经验集E的替换样本中随机采样n=sizeof(n)个样本对模型进行训练。Bao正是采用了这种方式来维护了汤普森采样的要求。</p><p data-pid="o1z80ju5">Bao的训练过程基本遵循一个经典的汤普森采样流程。当一个查询进来之后,Bao会通过给优化器提供优化提示集的方式生成很多的查询计划树,通过它的预测模型得到一个预测值。执行结束之后,Bao会获取实际执行的性能指标并将其加入到经验集当中。Bao会周期性的对模型进行重训练,通过对神经网络的权重进行采样的方式,来平衡exploration和exploitation。</p><p data-pid="wKrJLsUr">在实际的应用场景中,由于经验集会随之query的增加而不断变大,每一次采样与重训练的时间也会无限制的增长。因此Bao做了两个方面的调整。第一点是Bao只会在每n个query之后进行采样,从而减少采样带来的开销。其次,Bao只会存储最近的k个”经验“,从而控制了经验集过度增长的问题。</p><p data-pid="74jxNvSj">此外,Bao做的另外一个优化跟云深度相关。在如今的云服务上,GPU都是按秒计费。那么在Bao触发重训练的时候,可以将训练卸载到绑定的GPU上,从而尽量降低对整个系统对外服务的影响。当训练完成之后,会将新的参数从GPU换回CPU,然后解绑GPU结束这一次的重训练。</p><p data-pid="2n7LrmG4">Bao可以通过设置会话变量的方式,以每一条query为粒度进行开启和关闭。当开启的时候,会用上文描述的汤普森采样进行查询计划的选择。以如此细的粒度进行开关很大程度上避免了Bao对于短查询带来的额外训练开销,同时如果DBA已经结合业务对sql进行了深度的优化,那么关闭Bao可以避免打乱这些现有的优化。</p><p data-pid="eBcBlh0p">当Bao关闭的时候,也可以选择继续让它进行训练学习。每一条sql执行结束之后,它的相关性能信息会被记录到Bao的经验集当中,这个流程可以被看作是一个off-policy RL。这一部分也很有实际应用场景,通过让Bao学习DBA专家写的sql来让它更贴合业务,提供更强大的支持。</p><p data-pid="0pGvmQfN">在实际运行的时候,Bao有active和advisor两种模式。在Active模式下,顾名思义,Bao就像上文描述的那样正常工作,选择合适的查询计划,然后把结果反馈到Bao的结果集当中。在Advisor模式下,Bao不会介入到查询优化的过程中,而是让PostgreSQL的优化器全权接管。但是,当一个查询结束的时候,与上文类似,Bao依然会记录相关的性能信息,并在合适的实际训练模型。</p><p data-pid="cUS5BZ4V">另外一个有意思的地方在于,当用户用EXPLAIN来查看具体信息的时候,Bao对于这一条语句的优化建议以及预估运行时间也会展示给用户(也因此得名advisor模式)。感觉这一点对DBA十分友好啊,可以帮助探索性能边界。</p><p data-pid="rf78nY7f">首先需要通过实验回答的问题是,Bao是否像论文所说的那样适合落地。文中从以下几个方面进行了回答。</p><ul><li data-pid="D0Da3Ta7">云上的开销和性能。可以看到Bao在两个系统上都带来的非常大的提升,很大程度的降低了成本和执行时间。</li></ul><figure data-size="normal"><img src="https://picx.zhimg.com/v2-7938a6137c9ac0154ee7979581bbe1e8_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="505" data-rawheight="548" data-original-token="v2-cd5a79feddd61e108311e40cabe90dad" class="origin_image zh-lightbox-thumb" width="505" data-original="https://picx.zhimg.com/v2-7938a6137c9ac0154ee7979581bbe1e8_r.jpg?source=1940ef5c"/></figure><ul><li data-pid="jImczYyl">对于硬件的适配。可以看到相比PostgreSQL,Bao对于不同的硬件配置更加敏感,从而带来更高的性能。</li></ul><figure data-size="normal"><img src="https://picx.zhimg.com/v2-3742d6e364bdc3f91a5a6b565683ce7e_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="512" data-rawheight="552" data-original-token="v2-f89dac7ffa7682f65adc0bcb81cd8334" class="origin_image zh-lightbox-thumb" width="512" data-original="https://picx.zhimg.com/v2-3742d6e364bdc3f91a5a6b565683ce7e_r.jpg?source=1940ef5c"/></figure><ul><li data-pid="jBSCycVu">尾延时。由于生产环境更加关注尾延时的性能提升,所以这里论文也专门针对尾延时进行了实验。这里可以看到在不同的机器上,Bao都显著降低了P95,P99以及P99.5的延时。</li></ul><figure data-size="normal"><img src="https://picx.zhimg.com/v2-7c9a596143bb6ef2f6b3d6aa9cd2b59d_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1015" data-rawheight="473" data-original-token="v2-a69d1f27edfcc3e4a78e400346bb52d5" class="origin_image zh-lightbox-thumb" width="1015" data-original="https://picx.zhimg.com/v2-7c9a596143bb6ef2f6b3d6aa9cd2b59d_r.jpg?source=1940ef5c"/></figure><ul><li data-pid="g7HUZspb">训练与收敛时间。收敛时间其实是任何一个带有RL组件的系统最关键的指标,可以看到在经过最初两个小时的冷启动之后,Bao的执行速度迅速与PostgreSQL拉开了差距。由于这里使用的数据集是动态数据,会导致模型重训练,所以侧面佐证了模型训练和收敛时间对Bao的影响不大。</li></ul><figure data-size="normal"><img src="https://picx.zhimg.com/v2-5c2e7753e15f582f94f80d60b7f2350a_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="1020" data-rawheight="288" data-original-token="v2-d56ef7a838e24db38bc0ad0938452385" class="origin_image zh-lightbox-thumb" width="1020" data-original="https://picx.zhimg.com/v2-5c2e7753e15f582f94f80d60b7f2350a_r.jpg?source=1940ef5c"/></figure><p data-pid="vDu3Hj9g">然后是关于优化提示的分析:到底什么样的提示可以最大限度提高性能?实验发现,在85%的情况下,Bao对于查询优化的选择与PostgreSQL优化器构建的并不相同。在这其中,有将近75%选择了不同的查询路径,40%选择了不同的join顺序。而这40%的查询计划在前500条性能提升最大的sql中贡献了472条。所以可见,不同的join顺序对于查询时间的影响是巨大的。</p><p data-pid="_WcsFiCe">最后是关于Bao预测模型的性能分析。</p><ul><li data-pid="IzgKCTrO">通过将Bao与普通的随机森林和线性回归预测模型进行对比,发现引入神经网络对于整个预测的性能提升是最大的,也是必要的。</li></ul><figure data-size="normal"><img src="https://picx.zhimg.com/50/v2-f573ff175d237872808ff8997a30d236_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="345" data-rawheight="269" data-original-token="v2-1ac477a7fa41383eb26a686f72950e1c" class="content_image" width="345"/></figure><ul><li data-pid="3UeWqLbG">Bao的预测准确性随着时间的推移迅速降低。尽管在冷启动时会遇到Q-Error过高的问题,但是实验表明这也没有给整个系统带来严重的影响。</li></ul><figure data-size="normal"><img src="https://picx.zhimg.com/50/v2-c48137037d58ad64e2785c8f023293fd_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="329" data-rawheight="270" data-original-token="v2-665c264c656aa1c10c1de02be9381751" class="content_image" width="329"/></figure><ul><li data-pid="DeTqXVl6">Bao在其优化目标regret上的表现也远超PostgreSQL。</li></ul><figure data-size="normal"><img src="https://picx.zhimg.com/v2-c707f7b6db5f1ef5293b9a8cab72c106_r.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="509" data-rawheight="501" data-original-token="v2-3ea1950dc5e2980b0831f422d545c6b1" class="origin_image zh-lightbox-thumb" width="509" data-original="https://picx.zhimg.com/v2-c707f7b6db5f1ef5293b9a8cab72c106_r.jpg?source=1940ef5c"/></figure><ul><li data-pid="wqUgMxa7">通过允许自定义Bao的优化目标regret,可以使得整个系统更加适应云上多租户以及复杂workload的环境。例如,上图分别描述了regret定义为最小化CPU时间与最小化I/O请求。不同的目标会带来不同的表现,这就允许云上系统根据系统实时分配的资源情况进行动态调整,从而更好的发挥Bao的性能。</li></ul><p data-pid="phY5NZGB">总结来看,Bao为我们带来了一个适合工程落地的RL查询优化器的解法。我以后的工作可能也会在Bao上面继续做一些文章,例如将Bao带到云上,验证在多租户分布式云数据库上的可行性(现在正在和Amazon Redshift聊这个事情),以及探索Bao与传统优化器更加深度的结合,感兴趣的朋友可以关注一下。</p><p data-pid="25AkWfUH">关于Bao就讲到这里吧,得抓紧去复现了 :)</p>
<ul><li data-pid="_v-O7ILf">尽量将表字段定义为NOT NULL约束,这时由于在MySQL中含有空值的列很难进行查询优化,NULL值会使索引以及索引的统计信息变得很复杂。</li><li data-pid="wT_3NLhn">对于只包含特定类型的字段,可以使用enum、set 等符合数据类型。</li><li data-pid="mJg0tkmU">数值型字段的比较比字符串的比较效率高得多,字段类型尽量使用最小、最简单的数据类型。例如P地址可以使用int类型。</li><li data-pid="cNTmP6j6">尽量使用TINYINT、SMALLINT、MEDIUM_INT作为整数类型而非INT,如果非负则加上UNSIGNED</li><li data-pid="ngxCvmHF">VARCHAR的长度只分配真正需要的空间</li><li data-pid="tshpIvYN">尽量使用TIMESTAMP而非DATETIME,</li><li data-pid="sRj5fjU7">单表不要有太多字段,建议在20以内</li><li data-pid="oyDEBuyA">合理的加入冗余字段可以提高查询速度。</li></ul><p data-pid="tpjJnp4Y">垂直拆分按照字段进行拆分,其实就是把组成一行的多个列分开放到不同的表中,这些表具有不同的结构,拆分后的表具有更少的列。例如用户表中的一些字段可能经常访问,可以把这些字段放进一张表里。另外一些不经常使用的信息就可以放进另外一张表里。</p><p data-pid="-3u8waD-">插入的时候使用事务,也可以保证两表的数据一致。缺点也很明显,由于拆分出来的两张表存在一对一的关系,需要使用冗余字段,而且需要join操作,我们在使用的时候可以分别取两次,这样的来说既可以避免join操作,又可以提高效率。</p><p class="ztext-empty-paragraph"><br/></p><p data-pid="_kFg8iQj">水平拆分按照行进行拆分,常见的就是分库分表。以用户表为例,可以取用户ID,然后对ID取10的余数,将用户均匀的分配进这 0-9这10个表中。查找的时候也按照这种规则,又快又方便。</p><p data-pid="n4BwvBad">有些表业务关联比较强,那么可以使用按时间划分的。例如每天的数据量很大,需要每天新建一张表。这种业务类型就是需要高速插入,但是对于查询的效率不太关心。表越大,插入数据所需要索引维护的时间也就越长。</p><p data-pid="hx6lqsXQ">使用分区是大数据处理后的产物。比如系统用户的注册推广等等,会产生海量的日志,当然也可以按照时间水平拆分,建立多张表。但在实际操作中,容易发生忘记切换表导致数据错误。</p><p data-pid="5ZTVQQUZ">分区适用于例如日志记录,查询少。一般用于后台的数据报表分析。对于这些数据汇总需求,需要很多日志表去做数据聚合,我们能够容忍1s到2s的延迟,只要数据准确能够满足需求就可以。</p><p data-pid="v9IOkAKP">MySQL主要支持4种模式的分区:range分区、list预定义列表分区,hash 分区,key键值分区。</p><p data-pid="8rf67aXB">大型网站会有大量的并发访问,如果还是传统的数据结构,或者只是单单靠一台服务器扛,如此多的数据库连接操作,数据库必然会崩溃,数据丢失的话,后果更是不堪设想。这时候,我们需要考虑如何减少数据库的联接。 </p><p data-pid="huhg0Mc1">我们发现一般情况对数据库而言都是“读多写少”,也就说对数据库读取数据的压力比较大,这样分析可以采用数据库集群的方案。其中一个是主库,负责写入数据,我们称为写库;其它都是从库,负责读取数据,我们称为读库。这样可以缓解一台服务器的访问压力</p><p data-pid="QyyVazMB">如果访问量非常大,虽然使用读写分离能够缓解压力,但是一旦写操作一台服务器都不能承受了,这个时候我们就需要考虑使用多台服务器实现写操作。</p><p data-pid="XtyXE55Q"> 例如可以使用MyCat搭建MySql集群,对ID求3的余数,这样可以把数据分别存放到3台不同的服务器上,由MyCat负责维护集群节点的使用。</p><p></p>
<p data-pid="BIJYRkfi">作者介绍:本文整理自2019年第十届DTCC中国数据库技术大会OceanBase团队高级技术专家王国平(花名:溪峰)的演讲,本文将带读者深入了解OceanBase在查询优化器方面的设计思路和历经近十年时间提炼出的工程实践哲学。</p><p data-pid="e7-htwA1">Tips:您可以关注“OceanBase”微信公众号回复“dtcc”获取现场PPT</p><figure data-size="normal"><img src="https://pic2.zhimg.com/v2-e2ddeb9cebcce7128938d711bf5f81cd_r.jpg" data-caption="" data-size="normal" data-rawwidth="1280" data-rawheight="858" class="origin_image zh-lightbox-thumb" width="1280" data-original="https://pic2.zhimg.com/v2-e2ddeb9cebcce7128938d711bf5f81cd_r.jpg"/></figure><p data-pid="Cu9zpKjv">前言</p><p data-pid="4nTmUi7p">查询优化器是关系数据库系统的核心模块,是数据库内核开发的重点和难点,也是衡量整个数据库系统成熟度的“试金石”。</p><p data-pid="Ssn6q5j_">查询优化理论诞生距今已有四十来年,学术界和工业界其实已经形成了一套比较完善的查询优化框架(System-R的Bottom-up优化框架和Volcano/Cascade的Top-down优化框架),但围绕查询优化的核心难题始终没变——<b>如何利用有限的系统资源尽可能为查询选择一个“好”的执行计划</b>。</p><p data-pid="NeaOXHOj">近年来,新的存储结构(如LSM存储结构)的出现和分布式数据库的流行进一步加大了查询优化的复杂性,本文章结合OceanBase数据库过去近十年时间的实践经验,与大家一起探讨查询优化在实际应用场景中的挑战和解决方案。</p><p data-pid="8VfKxqGH"><b>查询优化器简介</b></p><p data-pid="9qVPtrRL">SQL是一种结构化查询语言,它只告诉数据库”想要什么”,但是它不会告诉数据库”如何获取”这个结果,这个"如何获取"的过程是由数据库的“大脑”查询优化器来决定的。在数据库系统中,一个查询通常会有很多种获取结果的方法,每一种获取的方法被称为一个"执行计划"。给定一个SQL,查询优化器首先会枚举出等价的执行计划。</p><p data-pid="CjF4_xin">其次,查询优化器会根据统计信息和代价模型为每个执行计划计算一个“代价”,这里的代价通常是指执行计划的执行时间或者执行计划在执行时对系统资源(CPU+IO+NETWORK)的占用量。最后,查询优化器会在众多等价计划中选择一个"代价最小"的执行计划。下图展示了查询优化器的基本组件和执行流程。</p><figure data-size="normal"><img src="https://pic4.zhimg.com/v2-28c7b41dde1316f3b9ebf0844337b887_r.jpg" data-caption="" data-size="normal" data-rawwidth="1147" data-rawheight="585" class="origin_image zh-lightbox-thumb" width="1147" data-original="https://pic4.zhimg.com/v2-28c7b41dde1316f3b9ebf0844337b887_r.jpg"/></figure><p data-pid="G6WSHUA_"><b>查询优化器面临的挑战</b></p><p data-pid="kJj525HD">查询优化自从诞生以来一直是数据库的难点,它面临的挑战主要体现在以下三个方面:</p><p data-pid="x2BuG9pm"><b>挑战一:精准的统计信息和代价模型</b></p><p data-pid="2iyOrdqS">统计信息和代价模型是查询优化器基础模块,它主要负责给执行计划计算代价。精准的统计信息和代价模型一直是数据库系统想要解决的难题,主要原因如下:</p><ol><li data-pid="uuNbvDSb"><b>统计信息:</b>在数据库系统中,统计信息搜集主要存在两个问题。首先,统计信息是通过采样搜集,所以必然存在采样误差。其次,统计信息搜集是有一定滞后性的,也就是说在优化一个SQL查询的时候,它使用的统计信息是系统前一个时刻的统计信息。</li><li data-pid="wQaGaULT"><b>选择率计算和中间结果估计:</b>选择率计算一直以来都是数据库系统的难点,学术界和工业界一直在研究能使选择率计算变得更加准确的方法,比如动态采样,多列直方图等计划,但是始终没有解决这个难题,比如连接谓词选择率的计算目前就没有很好的解决方法。</li><li data-pid="HR6l7Awz"><b>代价模型:</b>目前主流的数据库系统基本都是使用静态的代价模型,比如静态的buffer命中率,静态的IO RT,但是这些值都是随着系统的负载变化而变化的。如果想要一个非常精准的代价模型,就必须要使用动态的代价模型。</li></ol><p data-pid="a8qaI9xY"><b>挑战二:海量的计划空间</b></p><p data-pid="NZkvm_2M">复杂查询的计划空间是非常大的,在很多场景下,优化器甚至没办法枚举出所有等价的执行计划。下图展示了星型查询等价逻辑计划个数(不包含笛卡尔乘积的逻辑计划),而优化器真正的计划空间还得正交上算子物理实现,基于代价的改写和分布式计划优化。在如此海量的计划空间中,如何高效的枚举执行计划一直是查询优化器的难点。</p><figure data-size="normal"><img src="https://pic4.zhimg.com/v2-803f3fecbfb12bdf4a1c027a6836ed8f_r.jpg" data-caption="" data-size="normal" data-rawwidth="633" data-rawheight="341" class="origin_image zh-lightbox-thumb" width="633" data-original="https://pic4.zhimg.com/v2-803f3fecbfb12bdf4a1c027a6836ed8f_r.jpg"/></figure><p data-pid="gBqlilyY"><b>挑战三:高效的计划管理机制</b></p><p data-pid="Oxe2_4Mg">计划管理机制分成计划缓存机制和计划演进机制。</p><p data-pid="tnVSoXVm"><b>1. 计划缓存机制:</b>计划缓存根据是否参数化,优化一次/总是优化以及是否缓存可以划分成如下图所示的三种计划缓存方法。每个计划缓存方法都有各自的优缺点,不同的业务需求会选择不同的计划缓存方法。在蚂蚁/阿里很多高并发,低时延的业务场景下,就会选择<b>参数化+优化一次+缓存</b>的策略,那么就需要解决不同参数对应不同计划的问题(parametric query optimization),后面我们会详细讨论。</p><figure data-size="normal"><img src="https://pic2.zhimg.com/v2-413dd05a62b6f670558a408491e46585_r.jpg" data-caption="" data-size="normal" data-rawwidth="894" data-rawheight="237" class="origin_image zh-lightbox-thumb" width="894" data-original="https://pic2.zhimg.com/v2-413dd05a62b6f670558a408491e46585_r.jpg"/></figure><p data-pid="SWSU_TBD"><b>2. 计划演进机制:</b>计划演进是指对新生成计划进行验证,保证新计划不会造成性能回退。在数据库系统中, 新计划因为一些原因(比如统计信息刷新,schema版本升级)无时无刻都在才生,而优化器因为各种不精确的统计信息和代价模型始终是没办法百分百的保证新生成的计划永远都是最优的,所以就需要一个演进机制来保证新生成的计划不会造成性能回退。</p><p data-pid="Kd_NdLJg"><b>OceanBase查询优化器工程实践</b></p><p data-pid="2-Rr8AVC">下面我们来看一下OceanBase根据自身的框架特点和业务模型如何解决查询优化器所面临的挑战。</p><p data-pid="t2bwwocI">从统计信息和代价模型的维度看,OceanBase发明了基于LSM-TREE存储结构的基表访问路径选择。从计划空间的角度看,因为OceanBase原生就是一个分布式关系数据库系统,它必然要面临的一个问题就是分布式计划优化。从计划管理的角度看,OceanBase有一整套完善的计划管理机制。</p><p data-pid="2ZrmTDQZ">基表访问路径选择方法是指优化器选择索引的方法,其本质是要评估每一个索引的代价并选择代价最小的索引来访问数据库中的表。对于一个索引路径,它的代价主要由两部分组成,扫描索引的代价和回表的代价(如果一个索引对于一个查询来说不需要回表,那么就没有回表的代价)。</p><p data-pid="kOtaesEJ">通常来说,索引路径的代价取决于很多因素,比如扫描/回表的行数,投影的列数,谓词的个数等。为了简化我们的讨论,在下面的分析中,我们从行数这个维度来介绍这两部分的代价。 </p><ul><li data-pid="WSiS8eJS"><b>扫描索引的代价</b></li></ul><p data-pid="VbUQc1oT">扫描索引的代价跟扫描的行数成正比,而扫描的行数则是由一部分查询的谓词来决定,这些谓词定义了索引扫描开始和结束位置。理论上来说扫描的行数越多,执行时间就会越久。扫描索引的代价是顺序IO。</p><ul><li data-pid="qQeFqEhL"><b>回表的代价</b></li></ul><p data-pid="GtTxmZiB">回表的代价跟回表的行数也是正相关的,而回表的行数也是由查询的谓词来决定,理论上来说回表的行数越多,执行时间就会越久。回表的扫描是随机IO,所以回表一行的代价通常会比顺序扫描索引一行的代价要高。</p><p data-pid="DVaNNXxu">在传统关系数据库中,扫描索引的行数和回表的行数都是通过优化器中维护的统计信息来计算谓词选择率得到(或者通过一些更加高级的方法比如动态采样)。</p><p data-pid="NMxjAZEQ">举个简单的例子,给定联合索引(a,b)和查询谓词a > 1 and a < 5 and b < 5, 那么谓词 a > 1 and a < 5 定义了索引扫描开始和结束的位置,如果满足这两个条件的行数有1w行,那么扫描索引的代价就是1w行顺序扫描,如果谓词b < 5的选择率是0.5,那么回表的代价就是5k行的随机扫描。</p><p data-pid="VXFiThVu">那么问题来了:<b>传统的计算行数和代价的方法是否适合基于LSM-TREE的存储引擎?</b></p><p data-pid="wtQ6tNEL">LSM-TREE存储引擎把数据分为了两部分(如下图所示),静态数据(基线数据)和动态数据(增量数据)。其中静态数据不会被修改,是只读的,存储于磁盘;所有的增量修改操作(增、删、改)被记录在动态数据中,存储于内存。静态数据和增量数据会定期的合并形成新的基线数据。在LSM-TREE存储引擎中,对于一个查询操作,它需要合并静态数据和动态数据来形成最终的查询结果。</p><figure data-size="normal"><img src="https://pic4.zhimg.com/v2-5e9c0d307a3bb37f3e80f084a381d397_r.jpg" data-caption="" data-size="normal" data-rawwidth="793" data-rawheight="154" class="origin_image zh-lightbox-thumb" width="793" data-original="https://pic4.zhimg.com/v2-5e9c0d307a3bb37f3e80f084a381d397_r.jpg"/></figure><p data-pid="zDsq9rh6">考虑下图中LSM-TREE存储引擎基线数据被删除的一个例子。在该图中,基线中有10w行数据,增量数据中维护了对这10w行数据的删除操作。在这种场景下,这张表的总行数是0行,在传统的基于Buffer-Pool的存储引擎上,扫描会很快,也就是说行数和代价是匹配的。但是在LSM-TREE存储引擎中,扫描会很慢(10w基线数据+10w增加数据的合并),也就是行数和代价是不匹配的。</p><p data-pid="-sa1v7iZ">这个问题的本质原因是在基于LSM-TREE的存储引擎上,传统的基于动态采样和选择率信息计算出来的行数不足以反应实际计算代价过程中需要的行数。</p><p data-pid="_FC9-NJn">举个简单的例子,在传统的关系数据库中,我们插入1w行,然后删除其中1k行,那么计算代价的时候会用9k行去计算,在LSM-TREE的场景下,如果前面1w行是在基线数据里面,那么内存中会有额外的1k行,在计算代价的时候我们是需要用11k行去计算。</p><figure data-size="normal"><img src="https://pic2.zhimg.com/v2-a0f246edeade7784eeb1a3783e6a70fd_b.jpg" data-caption="" data-size="normal" data-rawwidth="306" data-rawheight="220" class="content_image" width="306"/></figure><p data-pid="Q10Tl0ye"><b>为了解决LSM-TREE存储引擎的计算代价行数和表中真实行数不一致的行为,OceanBase提出了“逻辑行”和“物理行”的概念以及计算它们的方法。</b>其中逻辑行可以理解为传统意义上的行数,物理行主要用于刻画LSM-TREE这种存储引擎在计算代价时需要真正访问的行数。</p><p data-pid="8NAzXxcR">再考虑上图中的例子,在该图中,逻辑行是0行,而物理行是20w行。给定索引扫描的开始/结束位置,对于基线数据,因为OceanBase为基线数据维护了块级别的统计信息,所以能很快的计算出来基线行数。对于增量数据,则通过动态采样方法获取增/删/改行数,最终两者合并就可以得到逻辑行和物理行。下图展示了OceanBase计算逻辑行和物理行的方法。</p><figure data-size="normal"><img src="https://pic4.zhimg.com/v2-7eb9b24f102dfaf6775ccf4b933e5257_r.jpg" data-caption="" data-size="normal" data-rawwidth="1051" data-rawheight="539" class="origin_image zh-lightbox-thumb" width="1051" data-original="https://pic4.zhimg.com/v2-7eb9b24f102dfaf6775ccf4b933e5257_r.jpg"/></figure><p data-pid="DUlASVrq">相比于传统的基表访问路径方法,OceanBase的基于逻辑行和物理行的方法有如下两个优势:</p><p data-pid="p9lRPDKc"><b>优势一:实时统计信息</b></p><p data-pid="UyhSOlgj">因为同时考虑了增量数据和基线数据,相当于统计信息是实时的,而传统方法的统计信息搜集是有一定的滞后性的(通常是一张表的增/删/修改操作到了一定程度,才会触发统计信息的重新搜集)。</p><p data-pid="F0pCVlOE"><b>优势二:解决了索引列上的谓词依赖关系</b></p><p data-pid="hfhm_VzA">考虑索引(a,b)以及查询条件 a=1 and b=1 , 传统的方法在计算这个查询条件的选择率的时候必然要考虑的一个问题是a和b是否存在依赖关系,然后再使用对应的方法(多列直方图或者动态采样)来提高选择率计算的正确率。OceanBase目前的估行方法默认能够解决a和b的依赖关系的场景。</p><p data-pid="9JYC_I53">OceanBase原生就有分布式的属性,那么它必然要解决的一个问题就是分布式计划优化。很多人认为分布式计划优化很难,无从下手,那么分布式计划优化跟本地优化到底有什么区别?分布式计划优化是否需要修改现有的查询优化框架来做优化?</p><p data-pid="rxvHUemL">在笔者看来,现有的查询优化框架完全有能力处理分布式计划优化,但是分布式计划优化会大大增加计划的搜索空间,主要原因如下:</p><ol><li data-pid="U5ZBIvlj">在分布式场景下,选择的是算子的分布式算法,而算子的分布式算法空间比算子本地算法的空间要大很多。下图展示了一个Hash Join在分布式场景下可以选择的分布式算法。</li></ol><figure data-size="normal"><img src="https://pic2.zhimg.com/v2-8906f7231dba586f7f3bd95ae39216f5_r.jpg" data-caption="" data-size="normal" data-rawwidth="1219" data-rawheight="291" class="origin_image zh-lightbox-thumb" width="1219" data-original="https://pic2.zhimg.com/v2-8906f7231dba586f7f3bd95ae39216f5_r.jpg"/></figure><p data-pid="db5iIYc6">2. 在分布式场景下,除了序这个物理属性之外,还增加了分区信息这个物理属性。分区信息主要包括如何分区以及分区的物理信息。分区信息决定了算子可以采用何种分布式算法。</p><p data-pid="hyJTK81D">3. 在分布式场景下,分区裁剪/并行度优化/分区内(间)并行等因素也会增大分布式计划的优化复杂度。</p><p data-pid="Q8HUxniF">OceanBase目前采用两阶段的方式来做分布式优化。在第一阶段,OceanBase基于所有表都是本地的假设生成一个最优本地计划。在第二阶段,OceanBase开始做并行优化, 用启发式规则来选择本地最优计划中算子的分布式算法。下图展示了OceanBase二阶段分布式计划的一个例子。</p><figure data-size="normal"><img src="https://pic3.zhimg.com/v2-f18530ea05532ea256aebad3245418da_r.jpg" data-caption="" data-size="normal" data-rawwidth="778" data-rawheight="133" class="origin_image zh-lightbox-thumb" width="778" data-original="https://pic3.zhimg.com/v2-f18530ea05532ea256aebad3245418da_r.jpg"/></figure><p class="ztext-empty-paragraph"><br/></p><figure data-size="normal"><img src="https://pic2.zhimg.com/v2-f98400e251ce25882aa7fb813b8d92c5_r.jpg" data-caption="" data-size="normal" data-rawwidth="956" data-rawheight="300" class="origin_image zh-lightbox-thumb" width="956" data-original="https://pic2.zhimg.com/v2-f98400e251ce25882aa7fb813b8d92c5_r.jpg"/></figure><p data-pid="GjCvxA8D">OceanBase二阶段的分布式计划优化方法能减少优化空间,降低优化复杂度,但是因为在第一阶段优化的时候没有考虑算子的分布式信息,所以可能导致生成的计划次优。目前OceanBase正在实现一阶段的分布式计划优化:</p><ol><li data-pid="W99-V9xU">在System-R的Bottom-up的动态规划算法中,枚举所有算子的所有分布式实现并且维护算子的物理属性。</li><li data-pid="oREO-mfM">在System-R的Bottom-up的动态规划算法中,对于每一个枚举的子集, 保留代价最小/有Interesting order/有Interesting分区的计划。</li></ol><p data-pid="GEFbPtQM">一阶段的分布式计划优化可能会导致计划空间增长很快,所以必须要有一些Pruning规则来减少计划空间或者跟本地优化一样在计划空间比较大的时候,使用遗传算法或者启发式规则来解决这个问题。</p><p data-pid="gPM7y4v3">OceanBase基于蚂蚁/阿里真实的业务场景,构建了一套完善的计划缓存机制和计划演进机制。</p><p data-pid="T2ju0FHW">如下图所示,OceanBase目前使用参数化计划缓存的方式。这里涉及到两个问题:为什么选择参数化以及为什么选择缓存?</p><ol><li data-pid="yUUdsKZO"><b>参数化:</b>在蚂蚁/阿里很多真实业务场景下,为每一个参数缓存一个计划是不切实际的。考虑一个根据订单号来查询订单信息的场景,在蚂蚁/阿里高并发的场景下,为每一个订单号换成一个计划是不切实际的,而且也不需要,因为一个带订单号的索引能解决所有参数的场景。</li><li data-pid="Ykv_Xb4p"><b>计划缓存:</b>计划缓存是因为性能的原因,对于蚂蚁/阿里很多真实业务场景来说,如果命中计划,那么一个查询的性能会在几百us,但是如果没有命中计划,那么性能大概会在几个ms。对于高并发,低时延的场景,这种性能优势是很重要的。</li></ol><figure data-size="normal"><img src="https://pic2.zhimg.com/v2-494e50c25589ca84255223e9e268a045_r.jpg" data-caption="" data-size="normal" data-rawwidth="1107" data-rawheight="149" class="origin_image zh-lightbox-thumb" width="1107" data-original="https://pic2.zhimg.com/v2-494e50c25589ca84255223e9e268a045_r.jpg"/></figure><p data-pid="AT5vIGJI">OceanBase使用参数化计划缓存的方式,但是在很多蚂蚁真实的业务场景下,对所有的参数使用同一个计划并不是最优的选择。考虑一个蚂蚁商户域的业务场景,这个场景以商户的维度去记录每一笔账单信息,商户可以根据这些信息做一些分析和查询。这种场景肯定会存在大小账号问题,如下图所示,淘宝可能贡献了50%的订单,LV可能只贡献了0.1%的订单。考虑查询“统计一个商户过去一年的销售额”,如果是淘宝和美团这种大商户,那么直接主表扫描会是一个合理的计划,对于LV这种小商户,那么走索引会是一个合理的计划。</p><figure data-size="normal"><img src="https://pic4.zhimg.com/v2-1d8ad92fa1cdeb19193f4d4949205737_r.jpg" data-caption="" data-size="normal" data-rawwidth="444" data-rawheight="182" class="origin_image zh-lightbox-thumb" width="444" data-original="https://pic4.zhimg.com/v2-1d8ad92fa1cdeb19193f4d4949205737_r.jpg"/></figure><p data-pid="_tVEzZPF">为了解决不同参数对应不同计划的问题,OceanBase实现了如下图所示的自适应计划匹配。该方法会通过直方图和执行反馈来监控每一个缓存的计划是否存在不同参数需要对应不同计划的问题。一旦存在,自适应计划匹配会通过渐进式的合并选择率空间来达到把整个选择率空间划分成若干个计划空间(每个空间对应一个计划)的目的。</p><figure data-size="normal"><img src="https://pic1.zhimg.com/v2-54ea58bbdfd95edc659d4a7041e906e8_r.jpg" data-caption="" data-size="normal" data-rawwidth="1158" data-rawheight="431" class="origin_image zh-lightbox-thumb" width="1158" data-original="https://pic1.zhimg.com/v2-54ea58bbdfd95edc659d4a7041e906e8_r.jpg"/></figure><p data-pid="YhtsBjrw">在蚂蚁/阿里很多高并发,低时延的业务场景下,OceanBase必须要保证新生成的计划不会导致性能回退。下图展示了OceanBase对新计划的演进过程。不同于传统的数据库系统采用定时任务和后台进程演进的方式,OceanBase会使用真实的流量来进行演进,这样的一个好处是可以及时的更新比较优的计划。比如当业务新建了一个更优的索引时,传统数据库系统并不能立刻使用该索引,需要在演进定时任务启动后才能演进验证使用,而OceanBase可以及时的使用该计划。</p><figure data-size="normal"><img src="https://pic3.zhimg.com/v2-a2654e3779125ecc96fbadec6a2e6a5a_r.jpg" data-caption="" data-size="normal" data-rawwidth="708" data-rawheight="406" class="origin_image zh-lightbox-thumb" width="708" data-original="https://pic3.zhimg.com/v2-a2654e3779125ecc96fbadec6a2e6a5a_r.jpg"/></figure><p data-pid="PKy3zmqO"><b>总结</b></p><p data-pid="nbfAB2CY">OceanBase查询优化器的实现立足于自身架构和业务场景特点,比如LSM-TREE存储结构、Share-Nothing的分布式架构和大规模的运维稳定性。OceanBase致力于打造基于OLTP和OLAP融合的查询优化器。从OLTP的角度看,我们立足于蚂蚁/阿里真实业务场景,完美承载了业务需求。从OLAP的角度看,我们对标商业数据库,进一步打磨我们HTAP的优化器能力。</p>
<blockquote data-pid="rcETDLqZ">本文整理自2019年第十届DTCC中国数据库技术大会OceanBase团队高级技术专家王国平(花名:溪峰)的演讲,本文将带读者深入了解OceanBase在查询优化器方面的设计思路和历经近十年时间提炼出的工程实践哲学。</blockquote><p data-pid="pOFB_uMq"><b>前言</b></p><p data-pid="P2NUL37I">查询优化器是关系数据库系统的核心模块,是数据库内核开发的重点和难点,也是衡量整个数据库系统成熟度的“试金石”。</p><p data-pid="IeZs-8zi">查询优化理论诞生距今已有四十来年,学术界和工业界其实已经形成了一套比较完善的查询优化框架(System-R 的 Bottom-up 优化框架和 Volcano/Cascade 的 Top-down 优化框架),但围绕查询优化的核心难题始终没变——<b>如何利用有限的系统资源尽可能为查询选择一个“好”的执行计划</b>。</p><p data-pid="gXZJSo_o">近年来,新的存储结构(如 LSM 存储结构)的出现和分布式数据库的流行进一步加大了查询优化的复杂性,本文章结合 OceanBase 数据库过去近十年时间的实践经验,与大家一起探讨查询优化在实际应用场景中的挑战和解决方案。</p><p data-pid="-xBELnSV"><b>查询优化器简介</b></p><p data-pid="rzvnjEZy">SQL 是一种结构化查询语言,它只告诉数据库”想要什么”,但是它不会告诉数据库”如何获取”这个结果,这个"如何获取"的过程是由数据库的“大脑”查询优化器来决定的。在数据库系统中,一个查询通常会有很多种获取结果的方法,每一种获取的方法被称为一个"执行计划"。给定一个 SQL,查询优化器首先会枚举出等价的执行计划。</p><p data-pid="z4oCTDWR">其次,查询优化器会根据统计信息和代价模型为每个执行计划计算一个“代价”,这里的代价通常是指执行计划的执行时间或者执行计划在执行时对系统资源(CPU + IO + NETWORK)的占用量。最后,查询优化器会在众多等价计划中选择一个"代价最小"的执行计划。下图展示了查询优化器的基本组件和执行流程。</p><figure data-size="normal"><img src="https://pic2.zhimg.com/v2-9f92799afc29ce53bd5263a53a245b5d_r.jpg" data-caption="" data-size="normal" data-rawwidth="1080" data-rawheight="551" class="origin_image zh-lightbox-thumb" width="1080" data-original="https://pic2.zhimg.com/v2-9f92799afc29ce53bd5263a53a245b5d_r.jpg"/></figure><p data-pid="RcG-VJyx"><b>查询优化器面临的挑战</b></p><p data-pid="vaflSwAe">查询优化自从诞生以来一直是数据库的难点,它面临的挑战主要体现在以下三个方面:</p><p data-pid="1Gu7OyVn"><b>挑战一:精准的统计信息和代价模型</b></p><p data-pid="skEnaltM">统计信息和代价模型是查询优化器基础模块,它主要负责给执行计划计算代价。精准的统计信息和代价模型一直是数据库系统想要解决的难题,主要原因如下:</p><p data-pid="Hs4T4hNE">1、<b>统计信息</b>:在数据库系统中,统计信息搜集主要存在两个问题。首先,统计信息是通过采样搜集,所以必然存在采样误差。其次,统计信息搜集是有一定滞后性的,也就是说在优化一个 SQL 查询的时候,它使用的统计信息是系统前一个时刻的统计信息。</p><p data-pid="iPiHQ0FQ">2、<b>选择率计算和中间结果估计</b>:选择率计算一直以来都是数据库系统的难点,学术界和工业界一直在研究能使选择率计算变得更加准确的方法,比如动态采样,多列直方图等计划,但是始终没有解决这个难题,比如连接谓词选择率的计算目前就没有很好的解决方法。</p><p data-pid="a60p39lv">3、<b>代价模型</b>:目前主流的数据库系统基本都是使用静态的代价模型,比如静态的 buffer 命中率,静态的 IO RT,但是这些值都是随着系统的负载变化而变化的。如果想要一个非常精准的代价模型,就必须要使用动态的代价模型。</p><p data-pid="gFthoVMa"><b>挑战二:海量的计划空间</b></p><p data-pid="MiSDxgen">复杂查询的计划空间是非常大的,在很多场景下,优化器甚至没办法枚举出所有等价的执行计划。下图展示了星型查询等价逻辑计划个数(不包含笛卡尔乘积的逻辑计划),而优化器真正的计划空间还得正交上算子物理实现,基于代价的改写和分布式计划优化。在如此海量的计划空间中,如何高效的枚举执行计划一直是查询优化器的难点。</p><figure data-size="normal"><img src="https://pic4.zhimg.com/v2-90c14acfb74c2c2c532d394f7d8f621f_r.jpg" data-caption="" data-size="normal" data-rawwidth="633" data-rawheight="341" class="origin_image zh-lightbox-thumb" width="633" data-original="https://pic4.zhimg.com/v2-90c14acfb74c2c2c532d394f7d8f621f_r.jpg"/></figure><p data-pid="JJdzUjX8"><b>挑战三:高效的计划管理机制</b></p><p data-pid="YJLzaCp4">计划管理机制分成计划缓存机制和计划演进机制。</p><p data-pid="nStzJuEA">1、<b>计划缓存机制</b>:计划缓存根据是否参数化,优化一次/总是优化以及是否缓存可以划分成如下图所示的三种计划缓存方法。每个计划缓存方法都有各自的优缺点,不同的业务需求会选择不同的计划缓存方法。在蚂蚁/阿里很多高并发,低时延的业务场景下,就会选择<b>参数化+优化一次+缓存</b>的策略,那么就需要解决不同参数对应不同计划的问题(parametric query optimization),后面我们会详细讨论。</p><figure data-size="normal"><img src="https://pic4.zhimg.com/v2-830b10effc4509ee6eabc848285a5d8b_r.jpg" data-caption="" data-size="normal" data-rawwidth="894" data-rawheight="237" class="origin_image zh-lightbox-thumb" width="894" data-original="https://pic4.zhimg.com/v2-830b10effc4509ee6eabc848285a5d8b_r.jpg"/></figure><p data-pid="GCPxDB1u">2、<b>计划演进机制</b>:计划演进是指对新生成计划进行验证,保证新计划不会造成性能回退。在数据库系统中, 新计划因为一些原因(比如统计信息刷新,schema版本升级)无时无刻都在才生,而优化器因为各种不精确的统计信息和代价模型始终是没办法百分百的保证新生成的计划永远都是最优的,所以就需要一个演进机制来保证新生成的计划不会造成性能回退。</p><p data-pid="KB5nETk9"><b>OceanBase 查询优化器工程实践</b></p><p data-pid="6WRG5jXZ">下面我们来看一下 OceanBase 根据自身的框架特点和业务模型如何解决查询优化器所面临的挑战。</p><p data-pid="eFTirxJQ">从统计信息和代价模型的维度看,OceanBase 发明了基于 LSM-TREE 存储结构的基表访问路径选择。从计划空间的角度看,因为 OceanBase 原生就是一个分布式关系数据库系统,它必然要面临的一个问题就是分布式计划优化。从计划管理的角度看,OceanBase 有一整套完善的计划管理机制。</p><p data-pid="oZTMBZyZ"><b>1基于 LSM - TREE 的基表访问路径选择</b></p><p data-pid="d5T5zS9K">基表访问路径选择方法是指优化器选择索引的方法,其本质是要评估每一个索引的代价并选择代价最小的索引来访问数据库中的表。对于一个索引路径,它的代价主要由两部分组成,扫描索引的代价和回表的代价(如果一个索引对于一个查询来说不需要回表,那么就没有回表的代价)。</p><p data-pid="LmrF0pzH">通常来说,索引路径的代价取决于很多因素,比如扫描/回表的行数,投影的列数,谓词的个数等。为了简化我们的讨论,在下面的分析中,我们从行数这个维度来介绍这两部分的代价。 </p><ul><li data-pid="3NEHbhnL"><b>扫描索引的代价</b></li></ul><p data-pid="liVhR1Gq">扫描索引的代价跟扫描的行数成正比,而扫描的行数则是由一部分查询的谓词来决定,这些谓词定义了索引扫描开始和结束位置。理论上来说扫描的行数越多,执行时间就会越久。扫描索引的代价是顺序 IO。</p><ul><li data-pid="pswEzwVP"><b>回表的代价</b></li></ul><p data-pid="z9dnvPHK">回表的代价跟回表的行数也是正相关的,而回表的行数也是由查询的谓词来决定,理论上来说回表的行数越多,执行时间就会越久。回表的扫描是随机 IO,所以回表一行的代价通常会比顺序扫描索引一行的代价要高。</p><p data-pid="c91MRZKl">在传统关系数据库中,扫描索引的行数和回表的行数都是通过优化器中维护的统计信息来计算谓词选择率得到(或者通过一些更加高级的方法比如动态采样)。</p><p data-pid="FRGYdwpY">举个简单的例子,给定联合索引(a,b)和查询谓词 a > 1 and a < 5 and b < 5, 那么谓词 a > 1 and a < 5 定义了索引扫描开始和结束的位置,如果满足这两个条件的行数有 1w 行,那么扫描索引的代价就是 1w 行顺序扫描,如果谓词 b < 5 的选择率是 0.5,那么回表的代价就是 5k 行的随机扫描。</p><p data-pid="z5nOtU5u">那么问题来了:<b>传统的计算行数和代价的方法是否适合基于 LSM-TREE 的存储引擎?</b></p><p data-pid="brqbJWFM">LSM-TREE 存储引擎把数据分为了两部分(如下图所示),<b>静态数据</b>(基线数据)和<b>动态数据</b>(增量数据)。其中静态数据不会被修改,是只读的,存储于磁盘;所有的增量修改操作(增、删、改)被记录在动态数据中,存储于内存。静态数据和增量数据会定期的合并形成新的基线数据。在 LSM-TREE 存储引擎中,对于一个查询操作,它需要合并静态数据和动态数据来形成最终的查询结果。</p><figure data-size="normal"><img src="https://pic1.zhimg.com/v2-11a424527f2342b479ebf4d02a5198bc_r.jpg" data-caption="" data-size="normal" data-rawwidth="793" data-rawheight="154" class="origin_image zh-lightbox-thumb" width="793" data-original="https://pic1.zhimg.com/v2-11a424527f2342b479ebf4d02a5198bc_r.jpg"/></figure><p data-pid="jgUDBSKb">考虑下图中 LSM-TREE 存储引擎基线数据被删除的一个例子。在该图中,基线中有 10w 行数据,增量数据中维护了对这 10w 行数据的删除操作。在这种场景下,这张表的总行数是 0 行,在传统的基于 Buffer-Pool 的存储引擎上,扫描会很快,也就是说行数和代价是匹配的。但是在 LSM-TREE 存储引擎中,扫描会很慢(10w 基线数据 + 10w 增加数据的合并),也就是行数和代价是不匹配的。</p><p data-pid="Z4P8OAGe">这个问题的本质原因是在基于 LSM-TREE 的存储引擎上,传统的基于动态采样和选择率信息计算出来的行数不足以反应实际计算代价过程中需要的行数。</p><p data-pid="r6zhNtBp">举个简单的例子,在传统的关系数据库中,我们插入 1w 行,然后删除其中 1k 行,那么计算代价的时候会用 9k 行去计算,在 LSM-TREE 的场景下,如果前面 1w 行是在基线数据里面,那么内存中会有额外的 1k 行,在计算代价的时候我们是需要用 11k 行去计算。</p><figure data-size="normal"><img src="https://pic4.zhimg.com/v2-cc18cb08d8bced58c7b3379eaf280c47_b.jpg" data-caption="" data-size="normal" data-rawwidth="306" data-rawheight="220" class="content_image" width="306"/></figure><p data-pid="m1ZzDOTT"><b>为了解决 LSM-TREE 存储引擎的计算代价行数和表中真实行数不一致的行为,OceanBase 提出了“逻辑行”和“物理行”的概念以及计算它们的方法</b>。其中逻辑行可以理解为传统意义上的行数,物理行主要用于刻画 LSM-TREE 这种存储引擎在计算代价时需要真正访问的行数。</p><p data-pid="tNMAsgtq">再考虑上图中的例子,在该图中,逻辑行是 0 行,而物理行是 20w 行。给定索引扫描的开始/结束位置,对于基线数据,因为 OceanBase 为基线数据维护了块级别的统计信息,所以能很快的计算出来基线行数。对于增量数据,则通过动态采样方法获取增/删/改行数,最终两者合并就可以得到逻辑行和物理行。下图展示了 OceanBase 计算逻辑行和物理行的方法。</p><figure data-size="normal"><img src="https://pic3.zhimg.com/v2-8dd6398da3a86111f98878e343f0e2b6_r.jpg" data-caption="" data-size="normal" data-rawwidth="1051" data-rawheight="539" class="origin_image zh-lightbox-thumb" width="1051" data-original="https://pic3.zhimg.com/v2-8dd6398da3a86111f98878e343f0e2b6_r.jpg"/></figure><p data-pid="kkdqVhcZ">相比于传统的基表访问路径方法,OceanBase 的基于逻辑行和物理行的方法有如下两个优势:</p><p data-pid="_tIzaXZT"><b>优势一:实时统计信息</b></p><p data-pid="eyTWrx7R">因为同时考虑了增量数据和基线数据,相当于统计信息是实时的,而传统方法的统计信息搜集是有一定的滞后性的(通常是一张表的增/删/修改操作到了一定程度,才会触发统计信息的重新搜集)。</p><p data-pid="JF6DTRCi"><b>优势二:解决了索引列上的谓词依赖关系</b></p><p data-pid="_06-_HRs">考虑索引(a,b)以及查询条件 a=1 and b=1 , 传统的方法在计算这个查询条件的选择率的时候必然要考虑的一个问题是 a 和 b 是否存在依赖关系,然后再使用对应的方法(多列直方图或者动态采样)来提高选择率计算的正确率。OceanBase 目前的估行方法默认能够解决 a 和 b 的依赖关系的场景。</p><p data-pid="optu2vZo"><b>2OceanBase 分布式计划优化</b></p><p data-pid="aS7KYBqe">OceanBase 原生就有分布式的属性,那么它必然要解决的一个问题就是分布式计划优化。很多人认为分布式计划优化很难,无从下手,那么分布式计划优化跟本地优化到底有什么区别?分布式计划优化是否需要修改现有的查询优化框架来做优化?</p><p data-pid="2Eb-qYzF">在笔者看来,现有的查询优化框架完全有能力处理分布式计划优化,但是分布式计划优化会大大增加计划的搜索空间,主要原因如下:</p><p data-pid="-lDZ9poQ">1、在分布式场景下,选择的是算子的分布式算法,而算子的分布式算法空间比算子本地算法的空间要大很多。下图展示了一个 Hash Join 在分布式场景下可以选择的分布式算法。</p><figure data-size="normal"><img src="https://pic3.zhimg.com/v2-4f11eee300f75a6b056f6f9780d19116_r.jpg" data-caption="" data-size="normal" data-rawwidth="1080" data-rawheight="258" class="origin_image zh-lightbox-thumb" width="1080" data-original="https://pic3.zhimg.com/v2-4f11eee300f75a6b056f6f9780d19116_r.jpg"/></figure><p data-pid="azl7-9yU">2、在分布式场景下,除了序这个物理属性之外,还增加了分区信息这个物理属性。分区信息主要包括如何分区以及分区的物理信息。分区信息决定了算子可以采用何种分布式算法。</p><p data-pid="t552oBvc">3、在分布式场景下,分区裁剪/并行度优化/分区内(间)并行等因素也会增大分布式计划的优化复杂度。</p><p data-pid="adwKbAHk">OceanBase 目前采用两阶段的方式来做分布式优化。在第一阶段,OceanBase 基于所有表都是本地的假设生成一个最优本地计划。在第二阶段,OceanBase 开始做并行优化, 用启发式规则来选择本地最优计划中算子的分布式算法。下图展示了 OceanBase 二阶段分布式计划的一个例子。</p><figure data-size="normal"><img src="https://pic1.zhimg.com/v2-80f8cbb1e1b64aee171155f1c7dd1bb8_r.jpg" data-caption="" data-size="normal" data-rawwidth="778" data-rawheight="133" class="origin_image zh-lightbox-thumb" width="778" data-original="https://pic1.zhimg.com/v2-80f8cbb1e1b64aee171155f1c7dd1bb8_r.jpg"/></figure><figure data-size="normal"><img src="https://pic2.zhimg.com/v2-c158f1d77d61d7caffbb05b3ece494b9_r.jpg" data-caption="" data-size="normal" data-rawwidth="956" data-rawheight="300" class="origin_image zh-lightbox-thumb" width="956" data-original="https://pic2.zhimg.com/v2-c158f1d77d61d7caffbb05b3ece494b9_r.jpg"/></figure><p data-pid="pHsGpvBa">OceanBase 二阶段的分布式计划优化方法能减少优化空间,降低优化复杂度,但是因为在第一阶段优化的时候没有考虑算子的分布式信息,所以可能导致生成的计划次优。目前 OceanBase 正在实现一阶段的分布式计划优化:</p><p data-pid="PyzpPs8Y">1、在 System-R 的 Bottom-up 的动态规划算法中,枚举所有算子的所有分布式实现并且维护算子的物理属性。</p><p data-pid="JttU0Xhv">2、在 System-R 的 Bottom-up 的动态规划算法中,对于每一个枚举的子集, 保留代价最小/有 Interesting order/有 Interesting 分区的计划。</p><p data-pid="XrutO_bq">一阶段的分布式计划优化可能会导致计划空间增长很快,所以必须要有一些 Pruning 规则来减少计划空间或者跟本地优化一样在计划空间比较大的时候,使用遗传算法或者启发式规则来解决这个问题。</p><p data-pid="4w9PwMdA"><b>3、OceanBase 计划管理机制</b></p><p data-pid="PxuS8Fdk">OceanBase 基于蚂蚁/阿里真实的业务场景,构建了一套完善的计划缓存机制和计划演进机制。</p><p data-pid="tYBu12m2"><b>OceanBase 计划缓存机制</b></p><p data-pid="uE4pAjdX">如下图所示,OceanBase 目前使用参数化计划缓存的方式。这里涉及到两个问题:为什么选择参数化以及为什么选择缓存?</p><p data-pid="yAj_p5SN">1、<b>参数化</b>:在蚂蚁/阿里很多真实业务场景下,为每一个参数缓存一个计划是不切实际的。考虑一个根据订单号来查询订单信息的场景,在蚂蚁/阿里高并发的场景下,为每一个订单号换成一个计划是不切实际的,而且也不需要,因为一个带订单号的索引能解决所有参数的场景。</p><p data-pid="WrAKXLth">2、<b>计划缓存</b>:计划缓存是因为性能的原因,对于蚂蚁/阿里很多真实业务场景来说,如果命中计划,那么一个查询的性能会在几百 us,但是如果没有命中计划,那么性能大概会在几个 ms。对于高并发,低时延的场景,这种性能优势是很重要的。</p><figure data-size="normal"><img src="https://pic2.zhimg.com/v2-0a16e0c865c934759f637ebd46ec9a55_r.jpg" data-caption="" data-size="normal" data-rawwidth="1080" data-rawheight="145" class="origin_image zh-lightbox-thumb" width="1080" data-original="https://pic2.zhimg.com/v2-0a16e0c865c934759f637ebd46ec9a55_r.jpg"/></figure><p data-pid="-lBWr3_f">OceanBase 使用参数化计划缓存的方式,但是在很多蚂蚁真实的业务场景下,对所有的参数使用同一个计划并不是最优的选择。考虑一个蚂蚁商户域的业务场景,这个场景以商户的维度去记录每一笔账单信息,商户可以根据这些信息做一些分析和查询。这种场景肯定会存在大小账号问题,如下图所示,淘宝可能贡献了 50% 的订单,LV 可能只贡献了 0.1% 的订单。考虑查询“统计一个商户过去一年的销售额”,如果是淘宝和美团这种大商户,那么直接主表扫描会是一个合理的计划,对于 LV 这种小商户,那么走索引会是一个合理的计划。</p><figure data-size="normal"><img src="https://pic1.zhimg.com/v2-7fcb3d2bbdab65c779fd2521249d0808_r.jpg" data-caption="" data-size="normal" data-rawwidth="444" data-rawheight="182" class="origin_image zh-lightbox-thumb" width="444" data-original="https://pic1.zhimg.com/v2-7fcb3d2bbdab65c779fd2521249d0808_r.jpg"/></figure><p data-pid="SYkmOGP3">为了解决不同参数对应不同计划的问题,OceanBase 实现了如下图所示的自适应计划匹配。该方法会通过直方图和执行反馈来监控每一个缓存的计划是否存在不同参数需要对应不同计划的问题。一旦存在,自适应计划匹配会通过渐进式的合并选择率空间来达到把整个选择率空间划分成若干个计划空间(每个空间对应一个计划)的目的。</p><figure data-size="normal"><img src="https://pic2.zhimg.com/v2-cc3fe2a27514c64f4321e47c9947a0a1_r.jpg" data-caption="" data-size="normal" data-rawwidth="1080" data-rawheight="402" class="origin_image zh-lightbox-thumb" width="1080" data-original="https://pic2.zhimg.com/v2-cc3fe2a27514c64f4321e47c9947a0a1_r.jpg"/></figure><p data-pid="aYWRNoI7"><b>OceanBase 计划演进机制</b></p><p data-pid="3WGA62aE">在蚂蚁/阿里很多高并发,低时延的业务场景下,OceanBase 必须要保证新生成的计划不会导致性能回退。下图展示了 OceanBase 对新计划的演进过程。不同于传统的数据库系统采用定时任务和后台进程演进的方式,OceanBase 会使用真实的流量来进行演进,这样的一个好处是可以及时的更新比较优的计划。比如当业务新建了一个更优的索引时,传统数据库系统并不能立刻使用该索引,需要在演进定时任务启动后才能演进验证使用,而 OceanBase 可以及时的使用该计划。</p><figure data-size="normal"><img src="https://pic2.zhimg.com/v2-ab3cd775f397966e2285b4166cc7f6c5_r.jpg" data-caption="" data-size="normal" data-rawwidth="708" data-rawheight="406" class="origin_image zh-lightbox-thumb" width="708" data-original="https://pic2.zhimg.com/v2-ab3cd775f397966e2285b4166cc7f6c5_r.jpg"/></figure><p data-pid="h0EVh561"><b>总结</b></p><p data-pid="BL4h3yIV">OceanBase 查询优化器的实现立足于自身架构和业务场景特点,比如 LSM-TREE 存储结构、Share-Nothing 的分布式架构和大规模的运维稳定性。OceanBase 致力于打造基于 OLTP 和 OLAP 融合的查询优化器。从 OLTP 的角度看,我们立足于蚂蚁/阿里真实业务场景,完美承载了业务需求。从 OLAP 的角度看,我们对标商业数据库,进一步打磨我们 HTAP 的优化器能力。</p><hr/><p data-pid="JrXLU-pT"><b>听直播送 Polo 衫!聊聊 OB 负载均衡的独特魅力</b></p><p data-pid="WaSnlNp2">5 月 30 日晚 7 点,OceanBase 的解决方案架构师庆涛将为大家带来《OceanBase 弹性伸缩和负载均衡简介及演示》的主题分享。OceanBase 的负载均衡和弹性伸缩能力是行业内独一无二的,既能发挥分布式的动态均衡在线扩展能力,又能给业务一定策略去干预。听干货,看直播,参与互动,还能获得 OB 全球限量版 Polo 衫!</p><p data-pid="SG_l9apA">扫描下方二维码,联系小助手加入 OceanBase 的技术直播群,赶快报名吧!</p><figure data-size="normal"><img src="https://pic3.zhimg.com/v2-be4d0d15f7f6b8414f028fd4843b24fe_r.jpg" data-caption="" data-size="normal" data-rawwidth="750" data-rawheight="1029" class="origin_image zh-lightbox-thumb" width="750" data-original="https://pic3.zhimg.com/v2-be4d0d15f7f6b8414f028fd4843b24fe_r.jpg"/></figure><p></p>
<p data-pid="Gbya7FRb">查询优化器在逻辑优化阶段主要解决的问题是:如何找出SQL语句等价的变换形式,使得SQL执行更高效。</p><p data-pid="XpmSpDi6">用于优化的思路包括:</p><figure data-size="normal"><img src="https://pic1.zhimg.com/v2-6aa4c793b4419c4a993ff4b82b2842cc_r.jpg" data-caption="" data-size="normal" data-rawwidth="1210" data-rawheight="722" class="origin_image zh-lightbox-thumb" width="1210" data-original="https://pic1.zhimg.com/v2-6aa4c793b4419c4a993ff4b82b2842cc_r.jpg"/></figure><p data-pid="yamPU-F1">各种逻辑优化技术依据关系代数和启发式规则进行。</p><p data-pid="0INyUhAg">查询优化技术的理论基础是关系代数。</p><p data-pid="0fxrejEO">关系数据库基于关系代数。关系数据库的对外接口是SQL语句,所以SQL语句中的DML、DQL基于关系代数实现了关系的运算。<br/><br/>作为数据库查询语言的基础,关系模型由关系数据结构、关系操作集合和关系完整性约束三部分组成。与关系模型有关的概念:</p><figure data-size="normal"><img src="https://pic2.zhimg.com/v2-1dca529fb48b54919c9a9ac261420dcd_r.jpg" data-caption="" data-size="normal" data-rawwidth="1234" data-rawheight="780" class="origin_image zh-lightbox-thumb" width="1234" data-original="https://pic2.zhimg.com/v2-1dca529fb48b54919c9a9ac261420dcd_r.jpg"/></figure><p data-pid="P_YzK7-Z">关系代数的运算符类别包括:</p><figure data-size="normal"><img src="https://pic1.zhimg.com/v2-98bdedd856b3a2b76e1628dc1f18391c_r.jpg" data-caption="" data-size="normal" data-rawwidth="1226" data-rawheight="598" class="origin_image zh-lightbox-thumb" width="1226" data-original="https://pic1.zhimg.com/v2-98bdedd856b3a2b76e1628dc1f18391c_r.jpg"/></figure><p data-pid="_uRMZBSb">基本关系运算与对应的SQL表(表2-1):</p><figure data-size="normal"><img src="https://pic4.zhimg.com/v2-55a70708e613090fdcc76016324a0fb3_r.jpg" data-caption="" data-size="normal" data-rawwidth="1152" data-rawheight="764" class="origin_image zh-lightbox-thumb" width="1152" data-original="https://pic4.zhimg.com/v2-55a70708e613090fdcc76016324a0fb3_r.jpg"/></figure><p data-pid="sWvu3Q2b"> 各种连接运算的语义表(表2-2):</p><figure data-size="normal"><img src="https://pic1.zhimg.com/v2-0dbfe399e431a6df157c260698c48314_r.jpg" data-caption="" data-size="normal" data-rawwidth="1500" data-rawheight="1342" class="origin_image zh-lightbox-thumb" width="1500" data-original="https://pic1.zhimg.com/v2-0dbfe399e431a6df157c260698c48314_r.jpg"/></figure><p data-pid="8Fm7QqC3">关系代数表达式的等价,就是相同的关系代替两个表达式中相应的关系,所得到的结果是相同的。两个关系表达式El和E2是等价的,记为E1≡E2。<br/><br/>查询语句可以表示为一棵二叉树,其中:</p><ul><li data-pid="3rjYXXzv">叶子是关系。</li><li data-pid="NKSoeAAa">内部结点是运算符(或称算子、操作符,如LEFTOUT JOIN),表示左右子树的运算方式。</li><li data-pid="psZD3kGk">子树是子表达式或SQL片段。</li><li data-pid="tdxhazdH">根结点是最后运算的操作符。</li><li data-pid="6hGMKYKw">根结点运算之后,得到的是SQL查询优化后的结果。</li><li data-pid="iWtKrRRE">这样一棵树就是一个查询的路径。</li><li data-pid="Fwdhp2N6">多个关系连接,连接顺序不同,可以得出多个类似的二叉树。</li><li data-pid="cGEYCpAM">查询优化就是找出代价最小的二叉树,即最优的查询路径。每条路径的生成,包括了单表扫描、两表连接、多表连接顺序、多表连接搜索空间等技术。</li><li data-pid="cf5MOGrl">基于代价估算的查询优化就是通过计算和比较,找出花费最少的最优二叉树。</li></ul><p data-pid="BleWsofP"><br/>最后两项,主要依据重写规则和物理查询优化中涉及的技术。</p><p data-pid="oA5TJjV2">不同运算符根据其特点,可以对查询语句做不同的优化。但优化的前提是:优化前和优化后的语义必须等价。<br/><br/>运算符主导的优化(表2-3):</p><figure data-size="normal"><img src="https://pic1.zhimg.com/v2-ffdd873c43e7d9f26e7319279db10530_r.jpg" data-caption="" data-size="normal" data-rawwidth="1183" data-rawheight="1032" class="origin_image zh-lightbox-thumb" width="1183" data-original="https://pic1.zhimg.com/v2-ffdd873c43e7d9f26e7319279db10530_r.jpg"/></figure><p data-pid="iu-2hAZ3"> 选择下推到集合的运算(表2-4):</p><figure data-size="normal"><img src="https://pic3.zhimg.com/v2-7979254fc5cee1f5daf7e337b07372a2_r.jpg" data-caption="" data-size="normal" data-rawwidth="1152" data-rawheight="214" class="origin_image zh-lightbox-thumb" width="1152" data-original="https://pic3.zhimg.com/v2-7979254fc5cee1f5daf7e337b07372a2_r.jpg"/></figure><p data-pid="8ip8EtsD"> 投影下推到集合的运算(表2-5):</p><figure data-size="normal"><img src="https://pic1.zhimg.com/v2-2899a15d616592a8c0a05a12f93cf108_r.jpg" data-caption="" data-size="normal" data-rawwidth="1152" data-rawheight="172" class="origin_image zh-lightbox-thumb" width="1152" data-original="https://pic1.zhimg.com/v2-2899a15d616592a8c0a05a12f93cf108_r.jpg"/></figure><p data-pid="2B2dz3V_"> 经过等价变换优化带来的好处,再加上避免了原始方式引入的坏处,使得查询效率明显获得提升。</p><p data-pid="E0eNKOha">因为运算符中考虑的子类型(见表2-3中的“子类型”列),实则是部分考虑了运算符间的关系、运算符和操作数间的关系,其本质是运算规则在起作用。所以前节考虑过关系代数运算规则对优化的作用,但不完整,这里补充余下的对优化有作用的主要关系代数运算规则。<br/><br/>运算规则主导的优化(表2-6):</p><figure data-size="normal"><img src="https://pic3.zhimg.com/v2-8071ee1e19af8d3014397d5dac2ba036_r.jpg" data-caption="" data-size="normal" data-rawwidth="1062" data-rawheight="1500" class="origin_image zh-lightbox-thumb" width="1062" data-original="https://pic3.zhimg.com/v2-8071ee1e19af8d3014397d5dac2ba036_r.jpg"/></figure><p data-pid="Cu0XHiu_">传统的联机事务处理(OLTP)使用基于选择(SELECT)、投影(PROJECT)、连接(JOIN)3种基本操作相结合的查询,称为SPJ查询。对这3种基本操作优化的方式如下:</p><figure data-size="normal"><img src="https://pic2.zhimg.com/v2-2d2aa5a7cde60aaa064b5c1285f1f9c1_r.jpg" data-caption="" data-size="normal" data-rawwidth="1200" data-rawheight="768" class="origin_image zh-lightbox-thumb" width="1200" data-original="https://pic2.zhimg.com/v2-2d2aa5a7cde60aaa064b5c1285f1f9c1_r.jpg"/></figure><p data-pid="Xct-XJCu">根据SQL语句的形式特点,还可以做如下区分:</p><ul><li data-pid="ZioZbFEk">针对SPJ的查询优化。基于选择、投影、连接3种基本操作相结合的查询。</li><li data-pid="G8WobzkY">针对非SPJ的查询优化。在SPJ的基础上存在GROUPBY操作的查询,这是一种较为复杂的查询。</li></ul><p data-pid="w5ojCWk-"><br/>针对SPJ和非SPJ的查询优化,其实是对以上多种操作的优化。“选择”和“投影”操作,可以在关系代数规则的指导下进行优化。表连接,需要多表连接的相关算法完成优化。其他操作的优化多是基于索引和代价估算完成的。</p><p data-pid="B5yccPOI">子查询在查询语句中经常出现,是比较耗时的操作。优化子查询对查询效率的提升有着直接的影响,所以子查询优化技术,是数据库查询优化引擎的重要研究内容。<br/><br/>子查询出现在SQL语句的位置和对优化的影响:</p><ul><li data-pid="19q3id1q">目标列。只能是标量子查询,否则数据库可能返回类似“错误:子查询必须只能返回一个字段”的提示。</li><li data-pid="3UKAhPdI">FROM子句。数据库可能返回类似“在FROM子句中的子查询无法参考相同查询级别中的关系”的提示,所以相关子查询不能出现在FROM子句中;非相关子查询出现在FROM子句中,可上拉子查询到父层,在多表连接时统一考虑连接代价后择优。</li><li data-pid="qRW65PZ-">WHERE子句。子查询是一个条件表达式的一部分,而表达式可以分解为操作符和操作数;根据参与运算的数据类型的不同,操作符也不尽相同,这对子查询均有一定的要求(如INT型的等值操作,要求子查询必须是标量子查询)。另外,子查询出现在WHERE子句中的格式也有用谓词指定的一些操作,如IN、BETWEEN、EXISTS等。</li><li data-pid="wloGwK9x">JOIN/ON子句。可以拆分为两部分,一是JOIN块类似于FROM子句,二是ON子句块类似于WHERE子句,这两部分都可以出现子查询。子查询的处理方式同FROM子句和WHERE子句。</li><li data-pid="E_dQs7cz">GROUPBY子句。目标列必须和GROUPBY关联。可将子查询写在GROUPBY位置处,但子查询用在GROUPBY处没有实用意义。</li><li data-pid="aq-LzJmw">HAVING子句。</li><li data-pid="GHnqTgj3">ORDERBY子句。可将子查询写在ORDERBY位置处。但ORDERBY操作是作用在整条SQL语句上的,子查询用在ORDERBY处没有实用意义。</li></ul><p data-pid="njlTxQ2f">按子查询中的关系对象与外层关系对象间的关系分类:</p><ul><li data-pid="fm9dvpZd"><b>相关子查询</b>。子查询的执行依赖于外层父查询的一些属性值。当父查询的参数改变时,子查询需要根据新参数值重新执行,如:</li></ul><div class="highlight"><pre><code class="language-sql"><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">t1</span> <span class="k">WHERE</span> <span class="n">col_1</span> <span class="o">=</span> <span class="k">ANY</span><span class="p">(</span><span class="k">SELECT</span> <span class="n">col_1</span> <span class="k">FROM</span> <span class="n">t2</span> <span class="k">WHERE</span> <span class="n">t2</span><span class="p">.</span><span class="n">col_2</span> <span class="o">=</span> <span class="n">t1</span><span class="p">.</span><span class="n">col_2</span><span class="p">);</span> <span class="cm">/*子查询语句中存在父查询的t1表的col_2列*/</span></code></pre></div><ul><li data-pid="8BYI48Pr"><b>非相关子查询</b>。子查询的执行不依赖于外层父查询的任何属性值,可独自求解,形成一个子查询计划先于外层的查询求解,如:</li></ul><div class="highlight"><pre><code class="language-sql"><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">t1</span> <span class="k">WHERE</span> <span class="n">col_1</span> <span class="o">=</span> <span class="k">ANY</span><span class="p">(</span><span class="k">SELECT</span> <span class="n">col_1</span> <span class="k">FROM</span> <span class="n">t2</span> <span class="k">WHERE</span> <span class="n">t2</span><span class="p">.</span><span class="n">col_2</span> <span class="o">=</span> <span class="mi">10</span><span class="p">);</span> <span class="cm">/*子查询语句中(t2)不存在父查询(t1)的属性*/</span></code></pre></div><p data-pid="WX5om8Ug"><br/>按特定谓词分类:</p><ul><li data-pid="5WVftEKA"><b>[NOT]IN/ALL/ANY/SOME子查询</b>。语义相近,表示“[取反]存在/所有/任何/任何”,左面是操作数,右面是子查询,是最常见的子查询类型之一。</li><li data-pid="QANuTmoC"><b>[NOT]EXISTS子查询</b>。半连接语义,表示“[取反]存在”,没有左操作数,右面是子查询,也是最常见的子查询类型之一。</li><li data-pid="OCiETmP_">其他子查询。除了上述两种外的所有子查询。</li></ul><p data-pid="s9TzEDco">按语句的构成复杂程度分类:</p><ul><li data-pid="eXsF8977"><b>SPJ子查询</b>。由选择、连接、投影操作组成的查询。</li><li data-pid="YGKCSor4"><b>GROUPBY子查询</b>。SPJ子查询加上分组、聚集操作组成的查询。</li><li data-pid="Z9lQJRJY"><b>其他子查询</b>。GROUPBY子查询中加上其他子句如Top-N、LIMIT/OFFSET、集合、排序等操作。后两种子查询有时合称非SPJ子查询。</li></ul><p data-pid="kmnrVtOy">按结果集的角度分类:</p><ul><li data-pid="MLlXhvTV"><b>标量子查询</b>。子查询返回的结果集类型是一个单一值(return a scalar,a single value)。</li><li data-pid="4p8w-Efs"><b>列子查询</b>。子查询返回的结果集类型是一条单一元组(return a single row)。</li><li data-pid="GJwzNd2m"><b>行子查询</b>。子查询返回的结果集类型是一个单一列(return a single column)。</li><li data-pid="UOblVfB8"><b>表子查询</b>。子查询返回的结果集类型是一个表(多行多列)(return a table,one or more rows ofone or more columns)。</li></ul><p data-pid="eOpF5fiI"><b>(1)做子查询优化的原因</b><br/>在数据库实现早期,查询优化器对子查询一般采用嵌套执行的方式,即对父查询中的每一行,都执行一次子查询,这样子查询会执行很多次,效率很低。<br/>对子查询进行优化,可能带来几个数量级的查询效率的提高。子查询转变成为连接操作之后,有如下好处:</p><ul><li data-pid="M7ixqYr6">子查询不用执行很多次。</li><li data-pid="k9OeZ1-B">优化器可以根据统计信息来选择不同的连接方法和不同的连接顺序。</li><li data-pid="utGPpnaV">子查询中的连接条件、过滤条件分别变成了父查询的连接条件、过滤条件,优化器可以对这些条件进行下推,以提高执行效率。</li></ul><p data-pid="bbly_pgX"><b>(2)子查询优化技术</b><br/>子查询优化技术的思路如下:</p><ul><li data-pid="hoW7X3yI"><b>子查询合并</b>(Subquery Coalescing)。在某些条件下,多个子查询能够合并成一个子查询。如:</li></ul><div class="highlight"><pre><code class="language-sql"><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">t1</span> <span class="k">WHERE</span> <span class="n">a1</span><span class="o"><</span><span class="mi">10</span> <span class="k">AND</span> <span class="p">(</span><span class="k">EXISTS</span> <span class="p">(</span><span class="k">SELECT</span> <span class="n">a2</span> <span class="k">FROM</span> <span class="n">t2</span> <span class="k">WHERE</span> <span class="n">t2</span><span class="p">.</span><span class="n">a2</span><span class="o"><</span><span class="mi">5</span> <span class="k">AND</span> <span class="n">t2</span><span class="p">.</span><span class="n">b2</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span> <span class="k">OR</span> <span class="k">EXISTS</span> <span class="p">(</span><span class="k">SELECT</span> <span class="n">a2</span> <span class="k">FROM</span> <span class="n">t2</span> <span class="k">WHERE</span> <span class="n">t2</span><span class="p">.</span><span class="n">a2</span><span class="o"><</span><span class="mi">5</span> <span class="k">AND</span> <span class="n">t2</span><span class="p">.</span><span class="n">b2</span><span class="o">=</span><span class="mi">2</span><span class="p">));</span>
<span class="c1">-- 可优化为:
</span><span class="c1"></span><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">t1</span> <span class="k">WHERE</span> <span class="n">a1</span><span class="o"><</span><span class="mi">10</span> <span class="k">AND</span> <span class="p">(</span><span class="k">EXISTS</span> <span class="p">(</span><span class="k">SELECT</span> <span class="n">a2</span> <span class="k">FROM</span> <span class="n">t2</span> <span class="k">WHERE</span> <span class="n">t2</span><span class="p">.</span><span class="n">a2</span><span class="o"><</span><span class="mi">5</span> <span class="k">AND</span> <span class="p">(</span><span class="n">t2</span><span class="p">.</span><span class="n">b2</span><span class="o">=</span><span class="mi">1</span> <span class="k">OR</span> <span class="n">t2</span><span class="p">.</span><span class="n">b2</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span> <span class="cm">/*大两个EXISTS子句合并为一个,条件也进行了合并*/</span></code></pre></div><ul><li data-pid="PTLALdMh"><b>子查询展开</b>(Subquery Unnesting)。又称子查询反嵌套或子查询上拉。把一些子查询置于外层的父查询中,作为连接关系与外层父查询并列,其实质是把某些子查询重写为等价的多表连接操作。有关的访问路径、连接方法和连接顺序可能被有效使用,使得查询语句的层次尽可能地减少。常见的IN/ANY/SOME/ALL/EXISTS依据情况转换为半连接(SEMI JOIN)、普通类型的子查询消除等情况属于此类,如:</li></ul><div class="highlight"><pre><code class="language-sql"><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">t1</span><span class="p">,</span> <span class="p">(</span><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">t2</span> <span class="k">WHERE</span> <span class="n">t2</span><span class="p">.</span><span class="n">a2</span><span class="o">></span><span class="mi">10</span><span class="p">)</span> <span class="n">v_t2</span> <span class="k">WHERE</span> <span class="n">t1</span><span class="p">.</span><span class="n">a1</span><span class="o"><</span><span class="mi">10</span> <span class="k">AND</span> <span class="n">v_t2</span><span class="p">.</span><span class="n">a2</span><span class="o"><</span><span class="mi">20</span><span class="p">;</span>
<span class="c1">--可优化为:
</span><span class="c1"></span><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">t1</span><span class="p">,</span> <span class="n">t2</span> <span class="k">WHERE</span> <span class="n">t1</span><span class="p">.</span><span class="n">a1</span><span class="o"><</span><span class="mi">10</span> <span class="k">AND</span> <span class="n">t2</span><span class="p">.</span><span class="n">a2</span><span class="o"><</span><span class="mi">20</span> <span class="k">AND</span> <span class="n">t2</span><span class="p">.</span><span class="n">a2</span><span class="o">></span> <span class="mi">10</span><span class="p">;</span><span class="cm">/*子查询变为了t1、t2表的连接操作,相当于把t2表从子查询中上拉了一层*/</span></code></pre></div><ul><li data-pid="XqZtt7HH"><b>聚集子查询消除</b>(Aggregate SubqueryElimination)。聚集函数上推,将子查询转变为一个新的不包含聚集函数的子查询,并与父查询的部分或者全部表做左外连接。通常,一些系统支持的是标量聚集子查询消除,如:</li></ul><div class="highlight"><pre><code class="language-sql"><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">t1</span> <span class="k">WHERE</span> <span class="n">t1</span><span class="p">.</span><span class="n">a1</span> <span class="o">></span> <span class="p">(</span><span class="k">SELECT</span> <span class="k">avg</span><span class="p">(</span><span class="n">t2</span><span class="p">.</span><span class="n">a2</span><span class="p">)</span> <span class="k">FROM</span> <span class="n">t2</span><span class="p">);</span></code></pre></div><ul><li data-pid="1qmZion0"><b>其他</b>。利用窗口函数消除子查询的技术(Remove Subquery using Window functions,RSW)、子查询推进(Push Subquery)等技术可用于子查询的优化。</li></ul><p data-pid="B9t6jfkR"><b>(3)子查询展开</b><br/>子查询展开是一种最为常用的子查询优化技术,子查询展开有以下两种形式:</p><ul><li data-pid="Q75dzMjc">如果子查询中出现了聚集、GROUPBY、DISTINCT子句,则子查询只能单独求解,不可以上拉到上层。</li><li data-pid="Kfqt_aya">如果子查询只是一个简单格式(SPJ格式)的查询语句,则可以上拉到上层,往往能提高查询效率。子查询上拉讨论的就是这种格式,也是子查询展开技术处理的范围。</li></ul><p data-pid="moy9kbv7">把子查询上拉到上层,前提是上拉(展开)后的结果不能带来多余的元组,所以需要遵循如下规则:</p><ul><li data-pid="1XXZbqB-">如果上层查询的结果没有重复(即SELECT子句中包含主码),则可以展开其子查询,并且展开后查询的SELECT子句前应加上DISTINCT标志。</li><li data-pid="Yl56q3ZD">如果上层查询的SELECT语句中有DISTINCT标志,则可以直接进行子查询展开。</li><li data-pid="d5ZYNNZY">如果内层查询结果没有重复元组,则可以展开。</li></ul><p data-pid="D67rFJOr">子查询展开的具体步骤如下:<br/>1)将子查询和上层查询的FROM子句连接为同一个FROM子句,并且修改相应的运行参数。<br/>2)将子查询的谓词符号进行相应修改(如:IN修改为=ANY)。<br/>3)将子查询的WHERE条件作为一个整体与上层查询的WHERE条件合并,并用AND条件连接词连接,从而保证新生成的谓词与原谓词的上下文意思相同,且成为一个整体。</p><p data-pid="JfnEtlr_">子查询的格式有多种,常见的子查询格式有IN类型、ALL/ANY/SOME类型、EXISTS类型。<br/><br/><b>(1)IN类型</b></p><p data-pid="_Zq8JH9d">IN类型有3种不同的格式:</p><div class="highlight"><pre><code class="language-sql"><span class="c1">-- 格式一:
</span><span class="c1"></span><span class="n">outer_expr</span> <span class="p">[</span><span class="k">NOT</span><span class="p">]</span> <span class="k">IN</span> <span class="p">(</span><span class="k">SELECT</span> <span class="n">inner_expr</span> <span class="k">FROM</span> <span class="p">...</span><span class="k">WHERE</span> <span class="n">subquery_where</span><span class="p">)</span>
<span class="c1">-- 格式二:
</span><span class="c1"></span><span class="n">outer_expr</span> <span class="o">=</span> <span class="k">ANY</span> <span class="p">(</span><span class="k">SELECT</span> <span class="n">inner_expr</span> <span class="k">FROM</span> <span class="p">...</span> <span class="k">WHERE</span> <span class="n">subquery_where</span><span class="p">)</span>
<span class="c1">--- 格式三:
</span><span class="c1"></span><span class="p">(</span><span class="n">oe_1</span><span class="p">,</span><span class="n">oe_N</span><span class="p">)</span> <span class="p">[</span><span class="k">NOT</span><span class="p">]</span> <span class="k">IN</span> <span class="p">(</span><span class="k">SELECT</span> <span class="n">ie_1</span><span class="p">,</span> <span class="n">ie_N</span> <span class="k">FROM</span> <span class="p">...</span> <span class="k">WHERE</span> <span class="n">subquery_where</span><span class="p">)</span></code></pre></div><p data-pid="1_2H8NnZ">IN类型子查询优化的情况表(表2-7):</p><figure data-size="normal"><img src="https://pic2.zhimg.com/v2-74ef8655c6b01c01b59c516a8c2a2c95_r.jpg" data-caption="" data-size="normal" data-rawwidth="1500" data-rawheight="236" class="origin_image zh-lightbox-thumb" width="1500" data-original="https://pic2.zhimg.com/v2-74ef8655c6b01c01b59c516a8c2a2c95_r.jpg"/></figure><p data-pid="2V4EWJg_"><b>情况一:</b>outer_expr和inner_expr均为非NULL值。<br/>优化后的表达式(外部条件outer_expr下推到子查询中):</p><div class="highlight"><pre><code class="language-sql"><span class="k">EXISTS</span> <span class="p">(</span><span class="k">SELECT</span> <span class="mi">1</span> <span class="k">FROM</span> <span class="p">...</span> <span class="k">WHERE</span> <span class="n">subquery</span><span class="p">.</span><span class="k">where</span> <span class="k">AND</span> <span class="n">outer_expr</span><span class="o">=</span><span class="n">inner_expr</span><span class="p">)</span>
<span class="c1">-- 即:
</span><span class="c1"></span><span class="k">EXISTS</span> <span class="p">(</span><span class="k">SELECT</span> <span class="mi">1</span> <span class="k">FROM</span> <span class="p">...</span> <span class="k">WHERE</span> <span class="n">subquery_where</span> <span class="k">AND</span> <span class="n">oe_1</span> <span class="o">=</span> <span class="n">ie_1</span> <span class="k">AND</span> <span class="p">...</span> <span class="k">AND</span> <span class="n">oe_N</span> <span class="o">=</span> <span class="n">ie_N</span><span class="p">)</span></code></pre></div><p data-pid="_ZT2cppm">子查询优化需要全部满足两个条件:</p><ul><li data-pid="3W1RJlgP">outer_expr和inner_expr不能为NULL。</li><li data-pid="FrKsCHcC">不需要从结果为FALSE的子查询中区分NULL。</li></ul><p data-pid="33C4kWZZ"><b>情况二:</b>outer_expr是非NULL值(情况一的两个转换条件中至少有一个不满足时)。<br/>优化后的表达式(外部条件outer_expr下推到子查询中,另外内部条件inner_expr为NULL):</p><div class="highlight"><pre><code class="language-sql"><span class="k">EXISTS</span> <span class="p">(</span><span class="k">SELECT</span> <span class="mi">1</span> <span class="k">FROM</span> <span class="p">...</span> <span class="k">WHERE</span> <span class="n">subquery</span><span class="p">.</span><span class="k">where</span> <span class="k">AND</span> <span class="p">(</span><span class="n">outer_expr</span> <span class="o">=</span> <span class="n">inner_expr</span> <span class="k">OR</span> <span class="n">inner_expr</span> <span class="k">IS</span> <span class="k">NULL</span><span class="p">))</span></code></pre></div><p data-pid="WHnFVA8J">假设outer_expr是非NULL值,但是如果outer_expr=inner_expr表达式不产生数据,则outer_expr IN (SELECT ...)的计算结果有如下情况:</p><ul><li data-pid="166TATjp">为NULL值。SELECT语句查询得到任意的行数据,inner_expr是NULL(outer_expr IN (SELECT ...)==NULL)。</li><li data-pid="JRuujUul">为FALSE值。SELECT语句产生非NULL值或不产生数据(outer_expr IN (SELECT ...)==FALSE)。</li></ul><p data-pid="rqmakiO5"><b>情况三:</b>outer_expr为NULL值。<br/>原先的表达式等价于:</p><div class="highlight"><pre><code class="language-sql"><span class="k">NULL</span> <span class="k">IN</span> <span class="p">(</span><span class="k">SELECT</span> <span class="k">inner</span><span class="p">.</span><span class="n">expr</span> <span class="k">FROM</span> <span class="p">...</span> <span class="k">WHERE</span> <span class="n">subquery_where</span><span class="p">)</span></code></pre></div><p data-pid="xNmYbTdK">假设outer_expr是NULL值,NULL IN(SELECTinner_expr...)的计算结果有如下情况:</p><ul><li data-pid="qI8lhW27">为NULL值。SELECT语句产生任意行数据。</li><li data-pid="QLokiWGm">为FALSE值。SELECT语句不产生数据。</li></ul><p data-pid="TlfEEO6q">还有需要说明的是:</p><ul><li data-pid="wQq5N1r_">谓词IN等价于=ANY。</li><li data-pid="JRcAV39I">带有谓词IN的子查询,如果满足上述3种情况,可以做等价变换。</li></ul><p data-pid="iBUCz03W"><br/><b>(2)ALL/ANY/SOME类型</b></p><p data-pid="yyupfO2M">ALL/ANY/SOME类型的子查询格式:</p><div class="highlight"><pre><code class="language-sql"><span class="n">outer_expr</span> <span class="k">operator</span> <span class="k">ANY</span> <span class="p">(</span><span class="n">subquery</span><span class="p">)</span> <span class="n">outer_expr</span> <span class="k">operator</span> <span class="k">SOME</span> <span class="p">(</span><span class="n">subquery</span><span class="p">)</span> <span class="n">outer_expr</span> <span class="k">operator</span> <span class="k">ALL</span> <span class="p">(</span><span class="n">subquery</span><span class="p">)</span></code></pre></div><p data-pid="5TrEoYpg">使用这类子查询需要注意:</p><ul><li data-pid="rvM1MrOJ">operator为操作符,通常可以是<、=<、>、>=中的任何一个。具体是否支持某个操作符,取决于表达式值的类型。</li><li data-pid="Syp1iYZ1">=ANY与IN含义相同,可以采用IN子查询优化方法。</li><li data-pid="xygrEume">SOME与ANY含义相同。</li><li data-pid="dtPaqK6l">NOT IN与<>ALL含义相同。</li><li data-pid="e2WNiDBs">NOT IN与<>ANY含义不相同。</li><li data-pid="ocuBN3pN"><> ANY表示不等于任何值。</li></ul><p data-pid="AZ5QSd7G">如果子查询中没有GROUPBY子句和聚集函数,则下面的表达式还可以使用聚集函数MAX/MIN做类似下面的等价转换:</p><ul><li data-pid="Br0ZX-7r">val>ALL(SELECT...)等价变化为val>MAX(SELECT...)</li><li data-pid="f1rUkJGU">val<ALL(SELECT...)等价变化为val<MIN(SELECT...)</li><li data-pid="sg5WKSL-">val>ANY(SELECT...)等价变化为val>MIN(SELECT...)</li><li data-pid="2SNZ_bi5">val<ANY(SELECT...)等价变化为val<MAX(SELECT...)</li><li data-pid="-6aFdwyA">val>=ALL(SELECT...)等价变化为val>=MAX(SELECT...)</li><li data-pid="iYhMQGwh">val<=ALL(SELECT...)等价变化为val<=MIN(SELECT...)</li><li data-pid="50QeZYOT">val>=ANY(SELECT...)等价变化为val>=MIN(SELECT...)</li><li data-pid="aHQB96-8">val<=ANY(SELECT...)等价变化为val<=MAX(SELECT...)</li></ul><p data-pid="DZfTF7Gq"><br/><b>(3)EXISTS类型</b></p><p data-pid="VB3UjJps">EXISTS类型的子查询格式:</p><div class="highlight"><pre><code class="language-sql"><span class="p">[</span><span class="k">NOT</span><span class="p">]</span> <span class="k">EXISTS</span> <span class="p">(</span><span class="n">subquery</span><span class="p">)</span></code></pre></div><p data-pid="lovRqYxI">需要注意几点:</p><ul><li data-pid="vL9U6HFN">EXISTS对于子查询而言,其结果值是布尔值;如果subquery有返回值,则整个EXISTS(subquery)的值为TRUE,否则为FALSE。</li><li data-pid="A28tp9Yc">EXISTS(subquery)不关心subquery返回的内容,这使得带有EXISTS(subquery)的子查询存在优化的可能。</li><li data-pid="9E-1ASA6">EXISTS(subquery)自身有着“半连接”的语义,所以,一些数据库实现代码中(如PostgreSQL),用半连接完成EXISTS(subquery)求值。</li><li data-pid="K9bEBjP-">NOT EXISTS(subquery)通常会被标识为“反半连接”处理。</li><li data-pid="9yqRWhY2">一些诸如IN(subquery)的子查询可以等价转换为EXISTS(subquery)格式,所以可以看到IN(subquery)的子查询可被优化为半连接实现的表连接。</li></ul><p data-pid="Rxhmq14h">视图重写就是将对视图的引用重写为对基本表的引用。视图重写后的SQL多被作为子查询进行进一步优化。所有的视图都可以被子查询替换,但不是所有的子查询都可以用视图替换。<br/><br/>从视图的构成形式可以分为:</p><ul><li data-pid="cDo-ubUC">简单视图,用SPJ格式构造的视图。</li><li data-pid="_XidGOP9">复杂视图,用非SPJ格式构造(带有GROUPBY等操作)的视图。</li></ul><p data-pid="_e2G1oI7">示例:</p><div class="highlight"><pre><code class="language-sql"><span class="c1">-- 创建表和视图
</span><span class="c1"></span><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">t_a</span><span class="p">(</span><span class="n">a</span> <span class="nb">INT</span><span class="p">,</span> <span class="n">b</span> <span class="nb">INT</span><span class="p">);</span>
<span class="k">CREATE</span> <span class="k">VIEW</span> <span class="n">v_a</span> <span class="k">AS</span> <span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">t_a</span><span class="p">;</span>
<span class="c1">-- 视图的查询命令
</span><span class="c1"></span><span class="k">SELECT</span> <span class="n">col_a</span> <span class="k">FROM</span> <span class="n">v_a</span> <span class="k">WHERE</span> <span class="n">col_b</span><span class="o">></span><span class="mi">100</span><span class="p">;</span>
<span class="c1">-- 视图重写后的形式
</span><span class="c1"></span><span class="k">SELECT</span> <span class="n">col_a</span> <span class="k">FROM</span> <span class="p">(</span>
<span class="k">SELECT</span> <span class="n">col_a</span><span class="p">,</span> <span class="n">col_b</span> <span class="k">FROM</span> <span class="n">t_a</span>
<span class="p">)</span> <span class="k">WHERE</span> <span class="n">col_b</span> <span class="o">></span> <span class="mi">100</span><span class="p">;</span>
<span class="c1">-- 优化后的等价形式:
</span><span class="c1"></span><span class="k">SELECT</span> <span class="n">col_a</span> <span class="k">FROM</span> <span class="n">t_a</span> <span class="k">WHERE</span> <span class="n">col_b</span> <span class="o">></span> <span class="mi">100</span><span class="p">;</span></code></pre></div><p data-pid="qGoTwSsW"><br/>简单视图能被较好地优化;但是复杂视图则不能被很好的优化器。像Oracle等商业数据库,提供了一些视图的优化技术,如“复杂视图合并”、“物化视图查询重写”等。但复杂视图优化技术还有待提高。</p><p data-pid="3z0AaReE">数据库执行引擎对一些谓词处理的效率要高些,因此把逻辑表达式重写成等价的且效率更高的形式,能有效提高查询执行效率。这就是等价谓词重写。<br/><br/><b>1. LIKE规则</b></p><p data-pid="wqZUdv4m">改写LIKE谓词为其他等价的谓词,以更好地利用索引进行优化。如:</p><div class="highlight"><pre><code class="language-sql"><span class="n">name</span> <span class="k">LIKE</span> <span class="s1">'Abc%'</span>
<span class="c1">-- 重写为
</span><span class="c1"></span><span class="n">name</span> <span class="o">>=</span> <span class="s1">'Abc'</span> <span class="k">AND</span> <span class="n">name</span> <span class="o"><</span> <span class="s1">'Abd'</span></code></pre></div><p data-pid="H2QCKIS6">应用LIKE规则的好处是:转换前只能进行全表扫描,如果目标列上存在索引,则转换后可以进行索引范围扫描。<br/><br/>LIKE匹配的表达式中,若没有通配符(%或_),则与=等价。如:</p><div class="highlight"><pre><code class="language-sql"><span class="n">name</span> <span class="k">LIKE</span> <span class="s1">'Abc'</span>
<span class="c1">-- 重写为
</span><span class="c1"></span><span class="n">name</span> <span class="o">=</span> <span class="s1">'Abc'</span></code></pre></div><p data-pid="zT9MM7BW"><br/><b>2. BETWEEN-AND规则</b></p><p data-pid="PchbB643">改写BETWEEN-AND谓词为其他等价的谓词,以更好地利用索引进行优化。与LIKE谓词的等价重写类似,如:</p><div class="highlight"><pre><code class="language-sql"><span class="n">sno</span> <span class="k">BETWEEN</span> <span class="mi">10</span> <span class="k">AND</span> <span class="mi">20</span>
<span class="c1">-- 重写为
</span><span class="c1"></span><span class="n">sno</span> <span class="o">>=</span> <span class="mi">10</span> <span class="k">AND</span> <span class="n">sno</span> <span class="o"><=</span> <span class="mi">20</span></code></pre></div><p data-pid="99QJoxBx">应用BETWEEN-AND规则的好处是:如果sno上建立了索引,则可以用索引扫描代替原来BETWEEN-AND谓词限定的全表扫描,从而提高了查询的效率。<br/></p><p data-pid="XquqNaN-"><b><a href="https://link.zhihu.com/?target=http%3A//3.IN" class=" external" target="_blank" rel="nofollow noreferrer"><span class="invisible">http://</span><span class="visible">3.IN</span><span class="invisible"></span></a>转换OR规则</b></p><p data-pid="CHi3BA1y">这里是指IN操作符操作,不是IN子查询。改写IN谓词为等价的OR谓词,以更好地利用索引进行优化。如:</p><div class="highlight"><pre><code class="language-sql"><span class="n">age</span> <span class="k">IN</span> <span class="p">(</span><span class="mi">8</span><span class="p">,</span><span class="mi">12</span><span class="p">,</span><span class="mi">21</span><span class="p">)</span>
<span class="c1">-- 重写为
</span><span class="c1"></span><span class="n">age</span> <span class="o">=</span> <span class="mi">8</span> <span class="k">OR</span> <span class="n">age</span> <span class="o">=</span> <span class="mi">12</span> <span class="k">OR</span> <span class="n">age</span> <span class="o">=</span> <span class="mi">21</span></code></pre></div><p data-pid="AeuX75hW">如果数据库对IN谓词只支持全表扫描且OR谓词中表的age列上存在索引,则转换后查询效率会提高。<br/><br/><b><a href="https://link.zhihu.com/?target=http%3A//4.IN" class=" external" target="_blank" rel="nofollow noreferrer"><span class="invisible">http://</span><span class="visible">4.IN</span><span class="invisible"></span></a>转换ANY规则</b></p><p data-pid="vMMOMjad">改写IN谓词为等价的ANY谓词。因为IN可以转换为OR,OR可以转为ANY,所以可以直接把IN转换为ANY。如</p><div class="highlight"><pre><code class="language-sql"><span class="n">age</span> <span class="k">IN</span> <span class="p">(</span><span class="mi">8</span><span class="p">,</span><span class="mi">12</span><span class="p">,</span><span class="mi">21</span><span class="p">)</span>
<span class="c1">-- 重写为
</span><span class="c1"></span><span class="n">age</span> <span class="k">ANY</span> <span class="p">(</span><span class="mi">8</span><span class="p">,</span><span class="mi">12</span><span class="p">,</span><span class="mi">21</span><span class="p">)</span></code></pre></div><p data-pid="7TW3_I33">效率是否能够提高,依赖于数据库对于ANY操作的支持情况。如PostgreSQL没有显式支持ANY操作,但是在内部实现时把IN操作转换为了ANY操作。<br/><br/><b>5.OR转换ANY规则</b></p><p data-pid="w6cgutv2">改写OR谓词为等价的ANY谓词,以更好地利用MIN/MAX操作进行优化。如:</p><div class="highlight"><pre><code class="language-sql"><span class="n">sal</span><span class="o">></span><span class="mi">1000</span> <span class="k">OR</span> <span class="n">dno</span><span class="o">=</span><span class="mi">3</span> <span class="k">AND</span> <span class="p">(</span><span class="n">sal</span><span class="o">></span><span class="mi">1100</span> <span class="k">OR</span> <span class="n">sal</span><span class="o">></span><span class="n">base_sal</span><span class="o">+</span><span class="mi">100</span><span class="p">)</span> <span class="k">OR</span> <span class="n">sal</span><span class="o">></span><span class="n">base_sal</span><span class="o">+</span><span class="mi">200</span> <span class="k">OR</span> <span class="n">sal</span><span class="o">></span><span class="n">base_sal</span><span class="o">*</span><span class="mi">2</span>
<span class="c1">-- 重写为
</span><span class="c1"></span><span class="n">dno</span><span class="o">=</span><span class="mi">3</span> <span class="k">AND</span> <span class="p">(</span><span class="n">sal</span> <span class="o">></span><span class="mi">1100</span> <span class="k">OR</span> <span class="n">sal</span><span class="o">></span><span class="n">base_sal</span><span class="o">+</span><span class="mi">100</span><span class="p">)</span> <span class="k">OR</span> <span class="n">sal</span> <span class="o">></span><span class="k">ANY</span> <span class="p">(</span><span class="mi">1000</span><span class="p">,</span> <span class="n">base_sal</span><span class="o">+</span><span class="mi">200</span><span class="p">,</span> <span class="n">base_sal</span><span class="o">*</span><span class="mi">2</span><span class="p">)</span></code></pre></div><p data-pid="GJmeuMow">OR转换ANY规则依赖于数据库对于ANY操作的支持情况。PostgreSQL V9.2.3和MySQLV5.6.10目前都不支持本条规则。<br/><br/><b>6.ALL/ANY转换集函数规则</b></p><p data-pid="X-mfG7pC">将ALL/ANY谓词改写为等价的聚集函数MIN/MAX谓词操作,以更好地利用MIN/MAX操作进行优化。如:</p><div class="highlight"><pre><code class="language-sql"><span class="n">sno</span> <span class="o">></span> <span class="k">ANY</span> <span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="mi">2</span><span class="o">*</span><span class="mi">5</span><span class="o">+</span><span class="mi">3</span><span class="p">,</span> <span class="n">sqrt</span><span class="p">(</span><span class="mi">9</span><span class="p">))</span>
<span class="c1">-- 重写为
</span><span class="c1"></span><span class="n">sno</span> <span class="o">></span> <span class="n">sqrt</span><span class="p">(</span><span class="mi">9</span><span class="p">)</span></code></pre></div><p data-pid="4JV1e4Ce">通常,聚集函数MAX()、MIN()等的执行效率比ANY、ALL谓词的执行效率高。如果有索引存在,求解MAX/MIN的效率更高。<br/><br/><b>7.NOT规则</b></p><p data-pid="qXB_wwZz">NOT谓词的等价重写。如下:</p><div class="highlight"><pre><code class="language-sql"><span class="k">NOT</span> <span class="p">(</span><span class="n">col_1</span> <span class="o">!=</span> <span class="mi">2</span><span class="p">)</span> <span class="err">重写为</span> <span class="n">col_1</span> <span class="o">=</span> <span class="mi">2</span>
<span class="k">NOT</span> <span class="p">(</span><span class="n">col_1</span> <span class="o">!=</span> <span class="n">col_2</span><span class="p">)</span> <span class="err">重写为</span> <span class="n">col_1</span> <span class="o">=</span> <span class="n">col_2</span>
<span class="k">NOT</span> <span class="p">(</span><span class="n">col_1</span> <span class="o">=</span> <span class="n">col_2</span><span class="p">)</span> <span class="err">重写为</span> <span class="n">col_1</span> <span class="o">!=</span> <span class="n">col_2</span>
<span class="k">NOT</span> <span class="p">(</span><span class="n">col_1</span> <span class="o"><</span> <span class="n">col_2</span><span class="p">)</span> <span class="err">重写为</span> <span class="n">col_1</span> <span class="o">>=</span> <span class="n">col_2</span>
<span class="k">NOT</span> <span class="p">(</span><span class="n">col_1</span> <span class="o">></span> <span class="n">col_2</span><span class="p">)</span> <span class="err">重写为</span> <span class="n">col_1</span> <span class="o"><=</span> <span class="n">col_2</span></code></pre></div><p data-pid="_1phDdIJ">如果在col_1上建立了索引,则可以用索引扫描代替原来的全表扫描,从而提高查询的效率。<br/><br/><b>8.OR重写并集规则</b></p><p data-pid="lJmRz_UF">OR条件重写为并集操作,形如以下SQL示例:</p><div class="highlight"><pre><code class="language-sql"><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">student</span> <span class="k">WHERE</span> <span class="p">(</span><span class="n">sex</span><span class="o">=</span><span class="s1">'f'</span> <span class="k">AND</span> <span class="n">sno</span><span class="o">></span><span class="mi">15</span><span class="p">)</span> <span class="k">OR</span> <span class="n">age</span><span class="o">></span><span class="mi">18</span><span class="p">;</span></code></pre></div><p data-pid="HNPKTKM5">假设所有条件表达式的列上都有索引(即sex列和age列上都存在索引),为了能利用索引处理上面的查询,可以将语句改成如下形式:</p><div class="highlight"><pre><code class="language-sql"><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">student</span> <span class="k">WHERE</span> <span class="n">sex</span><span class="o">=</span><span class="s1">'f'</span> <span class="k">and</span> <span class="n">sno</span><span class="o">></span><span class="mi">15</span> <span class="k">UNION</span> <span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">student</span> <span class="k">WHERE</span> <span class="n">age</span><span class="o">></span><span class="mi">18</span><span class="p">;</span></code></pre></div><p data-pid="j5b3wl5a">改写后的形式,可以分别利用列sex和age上的索引,进行索引扫描,然后再提供执行UNION操作获得最终结果。</p><p data-pid="Z8exfVFL">WHERE、HAVING和ON条件由许多表达式组成,利用等式和不等式的性质,可以将它们的条件化简,但不同数据库的实现可能不完全相同。不同数据库的实现可能不同。<br/><br/>条件化简的方式如下:</p><ul><li data-pid="MeXc4GDH"><b>把HAVING条件并入WHERE条件</b>。便于统一、集中化解条件子句,节约多次化解时间。仅在SQL语句中不存在GROUPBY条件或聚集函数的情况下,才能进行合并。</li><li data-pid="IZFjw2JD">去除表达式中冗余的括号。减少语法分析时产生的AND和OR树的层次。如((a AND b) AND (c AND d))就可以化简为a AND b AND c AND d。</li><li data-pid="3JVAQwbF"><b>常量传递</b>。对不同关系可以使得条件分离后有效实施“选择下推”,从而可以极大地减小中间关系的规模。如col_1=col_2 AND col_2=3就可以化简为col_1=3 AND col_2=3。操作符=、<、>、<=、>=、<>、LIKE中的任何一个,在col_1 <操作符> col_2条件中都会发生常量传递。</li><li data-pid="ceTXQegd"><b>消除死码</b>。去除不必要的条件。如WHERE (0>1 AND s1=5),0>1使得AND恒为假,则WHERE条件恒为假。去除可以加快查询执行的速度。</li><li data-pid="C4wd1lNC"><b>表达式计算</b>。对可以求解的表达式进行计算。如WHERE col_1=1+2变换为WHERE col_1=3。</li><li data-pid="Vwl-wA7p"><b>等式变换</b>。改变某些表的访问路径,如反转关系操作符的操作数的顺序。如-a=3可化简为a=-3。如果a上有索引,则可以利用索引扫描加快访问。</li><li data-pid="gB3jbNNG"><b>不等式变换</b>。去除不必要的重复条件。如a>10 AND b=6 AND a>2可化简为b=6 AND a>10。</li><li data-pid="pI5-GEsD"><b>布尔表达式变换</b>。布尔表达式还有如下规则指导化简。<br/></li><ul><li data-pid="lZZP5C3u"><b>谓词传递闭包</b>。如<、>等比较操作符具有传递性,可以化简表达式。如由a>b AND b>2可以推导出a>b AND b>2 AND a>2,a>2是一个隐含条件,这样把a>2和b>2分别下推到对应的关系上,就可以减少参与比较操作a>b的元组了。</li><li data-pid="ymAHHbw_"><b>任何一个布尔表达式都能被转换为一个等价的合取范式(CNF)</b>。因为合取项只要有一个为假,整个表达式就为假,故代码中可以在发现一个合取项为假时,即停止其他合取项的判断,以加快判断速度。另外因为AND操作符是可交换的,所以优化器可以按照先易后难的顺序计算表达式。</li><li data-pid="rDdYeOBu"><b>索引的利用</b>。如果一个合取项上存在索引,则先判断索引是否可用,如能利用索引快速得出合取项的值,则能加快判断速度。同理,OR表达式中的子项也可以利用索引。</li></ul></ul><p data-pid="8z-P5OGO"><b>1. 外连接消除的意义</b></p><p data-pid="H1ZhJN2e">外连接的左右子树不能互换,并且外连接与其他连接交换连接顺序时,必须满足严格的条件以进行等价变换。<br/><br/>查询重写的一项技术就是把外连接转换为内连接,对优化的意义如下:</p><ul><li data-pid="RKs7uBS5">查询优化器在处理外连接操作时所需执行的操作和时间多于内连接。</li><li data-pid="8rdXaNXN">优化器在选择表连接顺序时,可以有更多更灵活的选择,从而可以选择更好的表连接顺序,加快查询执行的速度。</li><li data-pid="IsLzkP8i">表的一些连接算法(如块嵌套连接和索引循环连接等)将规模小的或筛选条件最严格的表作为“外表”(放在连接顺序的最前面,是多层循环体的外循环层),可以减少不必要的IO开销,极大地加快算法执行的速度。</li></ul><p data-pid="roPRfQ-q">PostgreSQL外连接注释表(表2-8):</p><figure data-size="normal"><img src="https://pic2.zhimg.com/v2-d3af368bd40693e171ff3e44e71cc111_r.jpg" data-caption="" data-size="normal" data-rawwidth="1500" data-rawheight="375" class="origin_image zh-lightbox-thumb" width="1500" data-original="https://pic2.zhimg.com/v2-d3af368bd40693e171ff3e44e71cc111_r.jpg"/></figure><p data-pid="aRUfbpHO"><br/>分3种情况讨论表2-8,来弄明白,为什么外连接可以转换为内连接?(有点复杂,后续根据需要再详细了解)</p><p data-pid="7gXhH5ZS"><b>2. 外连接消除的条件</b></p><p data-pid="55Fc83xQ">外连接可转换为内连接的条件:WHERE子句中与内表相关的条件满足“空值拒绝”(reject-NULL条件)。一般认为下面任意一种情况满足空值拒绝:</p><ul><li data-pid="8zKJB_sR">条件可以保证从结果中排除外连接右侧(右表)生成的值为NULL的行,所以能使该查询在语义上等效于内连接。</li><li data-pid="ruzNUyOB">外连接的提供空值的一侧为另一侧的每行只返回一行。如果该条件为真,则不存在提供空值的行,并且外连接等价于内连接。</li></ul><p data-pid="El_gE8TH">当执行连接操作的次序不是从左到右逐个进行时,就说明这样的连接表达式存在嵌套。如:</p><div class="highlight"><pre><code class="language-sql"><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">t1</span> <span class="k">LEFT</span> <span class="k">JOIN</span> <span class="p">(</span><span class="n">t2</span> <span class="k">LEFT</span> <span class="k">JOIN</span> <span class="n">t3</span> <span class="k">ON</span> <span class="n">t2</span><span class="p">.</span><span class="n">b</span><span class="o">=</span><span class="n">t3</span><span class="p">.</span><span class="n">b</span><span class="p">)</span> <span class="k">ON</span> <span class="n">t1</span><span class="p">.</span><span class="n">a</span><span class="o">=</span><span class="n">t2</span><span class="p">.</span><span class="n">a</span> <span class="k">WHERE</span> <span class="n">t1</span><span class="p">.</span><span class="n">a</span><span class="o">></span><span class="mi">1</span></code></pre></div><p data-pid="W-tcTA-1">另外一种格式用括号把连接次序做了区分:</p><div class="highlight"><pre><code class="language-sql"><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">A</span> <span class="k">JOIN</span> <span class="p">(</span><span class="n">B</span> <span class="k">JOIN</span> <span class="k">C</span> <span class="k">ON</span> <span class="n">B</span><span class="p">.</span><span class="n">b1</span><span class="o">=</span><span class="k">C</span><span class="p">.</span><span class="n">c1</span><span class="p">)</span> <span class="k">ON</span> <span class="n">A</span><span class="p">.</span><span class="n">a1</span><span class="o">=</span><span class="n">B</span><span class="p">.</span><span class="n">b1</span> <span class="k">WHERE</span> <span class="n">A</span><span class="p">.</span><span class="n">a1</span> <span class="o">></span> <span class="mi">1</span><span class="p">;</span>
<span class="c1">-- 可以等价转换为
</span><span class="c1"></span><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">A</span> <span class="k">JOIN</span> <span class="n">B</span> <span class="k">JOIN</span> <span class="k">C</span> <span class="k">ON</span> <span class="n">B</span><span class="p">.</span><span class="n">b1</span><span class="o">=</span><span class="k">C</span><span class="p">.</span><span class="n">c1</span> <span class="k">ON</span> <span class="n">A</span><span class="p">.</span><span class="n">a1</span><span class="o">=</span><span class="n">B</span><span class="p">.</span><span class="n">b1</span> <span class="k">WHERE</span> <span class="n">A</span><span class="p">.</span><span class="n">a1</span> <span class="o">></span> <span class="mi">1</span><span class="p">;</span></code></pre></div><p data-pid="LvmW2skv">综上可得以下结论:</p><ul><li data-pid="wMI7B_Yf">如果连接表达式只包括内连接,括号可以去掉,这意味着表之间的次序可以交换,这是关系代数中连接的交换律的应用。</li><li data-pid="7kuHBZwB">如果连接表达式包括外连接,括号不可以去掉,意味着表之间的次序只能按照原语义进行,至多能执行的就是外连接向内连接转换的优化。</li></ul><p data-pid="GohDLn2D">连接的分类:</p><figure data-size="normal"><img src="https://pic3.zhimg.com/v2-1ac3b5c076f91ac6890293774f069ebe_r.jpg" data-caption="" data-size="normal" data-rawwidth="880" data-rawheight="1016" class="origin_image zh-lightbox-thumb" width="880" data-original="https://pic3.zhimg.com/v2-1ac3b5c076f91ac6890293774f069ebe_r.jpg"/></figure><p data-pid="4ceyv-1X"><br/>其中外链接的优化前面讨论过,下面分情况讨论其它可优化的连接。<br/><br/><b>情况一:</b>主外键关系的表进行的连接,可消除主键表,这不会影响对外键表的查询。</p><p data-pid="bCkoohI3">如:</p><div class="highlight"><pre><code class="language-sql"><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">B</span> <span class="p">(</span><span class="n">b1</span> <span class="nb">INT</span><span class="p">,</span> <span class="n">b2</span> <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">9</span><span class="p">),</span> <span class="k">PRIMARY</span> <span class="k">KEY</span><span class="p">(</span><span class="n">b1</span><span class="p">));</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">A</span> <span class="p">(</span><span class="n">a1</span> <span class="nb">INT</span><span class="p">,</span> <span class="n">a2</span> <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">9</span><span class="p">),</span> <span class="k">FOREIGN</span> <span class="k">KEY</span><span class="p">(</span><span class="n">a1</span><span class="p">)</span> <span class="k">REFERENCES</span> <span class="n">B</span><span class="p">(</span><span class="n">b1</span><span class="p">));</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="k">C</span> <span class="p">(</span><span class="n">c1</span> <span class="nb">INT</span><span class="p">,</span> <span class="n">c2</span> <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">9</span><span class="p">));</span></code></pre></div><p data-pid="cjqWjD9t"><br/><b>情况二:</b>唯一键作为连接条件,三表内连接可以去掉中间表(中间表的列只作为连接条件)。</p><p data-pid="hZHsx4WO">如:</p><div class="highlight"><pre><code class="language-sql"><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">A</span> <span class="p">(</span><span class="n">a1</span> <span class="nb">INT</span> <span class="k">UNIQUE</span><span class="p">,</span> <span class="n">a2</span> <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">9</span><span class="p">),</span><span class="n">a3</span> <span class="nb">INT</span><span class="p">);</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">B</span> <span class="p">(</span><span class="n">b1</span> <span class="nb">INT</span> <span class="k">UNIQUE</span><span class="p">,</span> <span class="n">b2</span> <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">9</span><span class="p">),</span><span class="n">c2</span> <span class="nb">INT</span><span class="p">);</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="k">C</span> <span class="p">(</span><span class="n">c1</span> <span class="nb">INT</span> <span class="k">UNIQUE</span><span class="p">,</span> <span class="n">c2</span> <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">9</span><span class="p">),</span><span class="n">c3</span> <span class="nb">INT</span><span class="p">);</span></code></pre></div><p data-pid="o3AzZYKO">B的列在WHERE条件子句中只作为等值连接条件存在,则查询可以去掉对B的连接操作。</p><div class="highlight"><pre><code class="language-sql"><span class="k">SELECT</span> <span class="n">A</span><span class="p">.</span><span class="o">*</span><span class="p">,</span> <span class="k">C</span><span class="p">.</span><span class="o">*</span> <span class="k">FROM</span> <span class="n">A</span> <span class="k">JOIN</span> <span class="n">B</span> <span class="k">ON</span> <span class="p">(</span><span class="n">a1</span><span class="o">=</span><span class="n">b1</span><span class="p">)</span> <span class="k">JOIN</span> <span class="k">C</span> <span class="k">ON</span> <span class="p">(</span><span class="n">b1</span><span class="o">=</span><span class="n">c1</span><span class="p">);</span>
<span class="c1">-- 相当于
</span><span class="c1"></span><span class="k">SELECT</span> <span class="n">A</span><span class="p">.</span><span class="o">*</span><span class="p">,</span> <span class="k">C</span><span class="p">.</span><span class="o">*</span> <span class="k">FROM</span> <span class="n">A</span> <span class="k">JOIN</span> <span class="k">C</span> <span class="k">ON</span> <span class="p">(</span><span class="n">a1</span><span class="o">=</span> <span class="n">c1</span><span class="p">);</span></code></pre></div><p data-pid="Hjz1AK5S"><br/><b>情况三:</b>其他一些特殊形式</p><p data-pid="zBd_b3FH">如:</p><div class="highlight"><pre><code class="language-sql"><span class="k">SELECT</span> <span class="k">MAX</span><span class="p">(</span><span class="n">a1</span><span class="p">)</span> <span class="k">FROM</span> <span class="n">A</span><span class="p">,</span> <span class="n">B</span><span class="p">;</span><span class="cm">/* 在这样格式中的MIN、MAX函数操作可以消除连接,去掉B表不影响结果,其他聚集函数不可以*/</span>
<span class="k">SELECT</span> <span class="k">DISTINCT</span> <span class="n">a3</span> <span class="k">FROM</span> <span class="n">A</span><span class="p">,</span> <span class="n">B</span><span class="p">;</span> <span class="cm">/* 对连接结果中的a3列执行去重操作*/</span>
<span class="k">SELECT</span> <span class="n">a1</span> <span class="k">FROM</span> <span class="n">A</span><span class="p">,</span> <span class="n">B</span> <span class="k">GROUP</span> <span class="k">BY</span> <span class="n">a1</span><span class="p">;</span><span class="cm">/* 对连接结果中的a1列执行分组操作*/</span></code></pre></div><p data-pid="oaX3tugj">语义优化包括以下基本概念:</p><ul><li data-pid="I-KGRV0e">语义转换,因为完整性限制等原因使得一个转换成立的情况。</li><li data-pid="FSUC6R1Q">语义优化,因为语义转换形成的优化。</li></ul><p data-pid="IxY_FEgp">语义转换是根据完整性约束等信息对“某特定语义”进行推理,得到一种查询效率不同但结果相同的查询。<br/>语义优化是从语义的角度对SQL进行优化,不是一种形式上的优化,所以其优化的范围可能覆盖其他类型的优化范围。<br/><br/>语义优化常见的方式如下:</p><ul><li data-pid="4FIbYvyk"><b>连接消除</b>(Join Elimination)。对一些连接操作先不必评估代价,根据已知信息(主要依据完整性约束等)能推知结果或得到一个简化的操作。如“视图重写"中的例子。</li><li data-pid="EXIFJjAO"><b>连接引入</b>(Join Introduction)。增加连接有助于原关系变小或原关系的选择率降低。</li><li data-pid="llmdc7eU"><b>谓词引入</b>(Predicate Introduction)。根据完整性约束等信息引入新谓词,如引入基于索引的列,可能使得查询更快。如一个表上,有c1<c2的列约束,c2列上存在一个索引,查询语句中的WHERE条件有c1>200,则可以推知c2>200,WHERE条件变更为c2>200 AND c1>200 AND c1<c2,由此可以利用c2列上的索引,对查询语句进行优化。如果c2列上的索引的选择率很低,则优化效果会更高。</li><li data-pid="8lt0_amr"><b>检测空回答集</b>(Detecting the Empty AnswerSet)。查询语句中的谓词与约束相悖,可以推知条件结果为FALSE,也许最终的结果集能为空;如CHECK约束限定score列的范围是60到100,而一个查询条件是score<60,则能立刻推知条件不成立。</li><li data-pid="1BQjrAHI"><b>排序优化</b>(Order Optimizer)。ORDERBY操作通常由索引或排序(sort)完成;如果能够利用索引,则排序操作可省略。另外,结合分组等操作,考虑ORDERBY操作的优化。</li><li data-pid="HK1w5-Dj"><b>唯一性使用</b>(Exploiting Uniqueness)。利用唯一性、索引等特点,检查是否存在不必要的DISTINCT操作,如在主键上执行DISTINCT操作,若有则可以把DISTINCT消除掉。</li></ul><p data-pid="TUHyUhWF">非SPJ查询,查询中包含GROUPBY子句。<br/><br/><b>1. GROUPBY优化</b></p><p data-pid="LUd1UQ-E">可考虑分组转换技术,即对分组操作、聚集操作与连接操作的位置进行交换。常见方式如下:</p><ul><li data-pid="CV-mNpr3"><b>分组操作下移</b>。GROUPBY操作可能较大幅度地减少关系元组的个数,如果能够对某个关系先进行分组操作,然后再进行表之间的连接,很可能提高连接效率。这种优化方式是把分组操作提前执行。下移的含义,是在查询树上让分组操作尽量靠近叶子结点,使得分组操作的结点低于一些选择操作。</li><li data-pid="AKcRhjtQ"><b>分组操作上移</b>。如果连接操作能够过滤掉大部分元组,则先进行连接后,再进行GROUPBY操作,可能提高分组操作的效率。这种优化方式是把分组操作置后执行。上移的含义和下移正好相反。</li></ul><p data-pid="e1xhx-8M">另外,GROUPBY、ORDERBY优化的另外一个思路是尽量利用索引。</p><p data-pid="pvDZlL9q"><b>2. ORDERBY优化</b></p><p data-pid="ZrfLjmrD">对于ORDERBY的优化,可有如下方面的考虑:</p><ul><li data-pid="uLIpDkiH"><b>排序消除</b>(Order By Elimination,OBYE)。优化器在生成执行计划前,将语句中没有必要的排序操作消除(如利用索引)。</li><li data-pid="9rB0_80r"><b>排序下推</b>(Sort push down)。把排序操作尽量下推到基表中,有序的基表进行连接后的结果符合排序的语义,这样能避免在最终的大的连接结果集上执行排序操作。</li></ul><p data-pid="I_r0bMA8"><b>3. DISTICT优化</b></p><p data-pid="HQN7E7YC">可考虑如下方面:</p><ul><li data-pid="bDZpRMhG"><b>DISTINCT消除</b>(Distinct Elimination)。如果表中存在主键、唯一约束、索引等,则可以消除查询语句中的DISTINCT(这种优化方式本质上是语义优化研究的范畴)。</li><li data-pid="i8qNuMl2"><b>DISTINCT推入</b>(Distinct Push Down)。生成含DISTINCT的反半连接查询执行计划时,先进行反半连接再进行DISTICT操作,也许先执行DISTICT操作再执行反半连接更优,这是利用连接语义上确保唯一功能特性进行DISTINCT的优化。</li><li data-pid="yKGS0DGc"><b>DISTINCT迁移</b>(Distinct Placement)。对连接操作的结果执行DISTINCT,可能把DISTINCT移到一个子查询中优先进行(有的书籍称之为“DISTINCT配置”)。</li></ul><p data-pid="QPxBUcPl">逻辑优化阶段使用的启发式规则通常包括如下两类。<br/><br/>1. 一定能带来优化效果的,主要包括:</p><ul><li data-pid="y6rABibD">优先做选择和投影(连接条件在查询树上下推)。</li><li data-pid="ocSnTh9n">子查询的消除。</li><li data-pid="JYJbqpRO">嵌套连接的消除。</li><li data-pid="yz4enunK">外连接的消除。</li><li data-pid="44NxZwXi">连接的消除。</li><li data-pid="A9DNk8Hf">使用等价谓词重写对条件化简。</li><li data-pid="AAOhwPwj">语义优化。</li><li data-pid="gcL92Ran">剪掉冗余操作(一些剪枝优化技术)、最小化查询块。</li></ul><p data-pid="8xJUVHT6">2. 变换未必会带来性能的提高,需根据代价选择</p><ul><li data-pid="CjFMrmqT">分组的合并。</li><li data-pid="TuCGM_L1">借用索引优化分组、排序、DISTINCT等操作。</li><li data-pid="PdtvyPSi">对视图的查询变为基于表的查询。</li><li data-pid="GcNbaFv-">连接条件的下推。</li><li data-pid="le9FHsmA">分组的下推。</li><li data-pid="dxGE9_LD">连接提取公共表达式。</li><li data-pid="LpIPfb34">谓词的上拉。</li><li data-pid="NLewi6DE">用连接取代集合操作。</li><li data-pid="sOVqwGCf">用UNIONALL取代OR操作。</li></ul><ul><li data-pid="0xQ94c0v">《数据库查询优化的艺术:原理解析和SQL性能优化》,李海翔</li></ul><p></p>
<p data-pid="8cnRmblg"><b>问题背景</b><sup data-text="Ganski, Richard A., and Harry KT Wong. "Optimization of nested SQL queries revisited." ACM SIGMOD Record 16.3 (1987): 23-33." data-url="https://dl.acm.org/doi/pdf/10.1145/38714.38723" data-draft-node="inline" data-draft-type="reference" data-numero="1">[1]</sup></p><p data-pid="BC7_xM-v">嵌套查询简单来说就是有子查询的SQL语句,子查询可以出现在SLECT, FROM或者WHERE 子句中,也可以单独用WITH子句来定义一个子查询。使用时子查询可以将一个复杂的查询拆分成一个个独立的部分,逻辑上更易于理解以及代码的维护和重复使用。有利于程序查询缓存,减少锁的竞争,减少查询冗余,应用层面相当于实现哈希关联。更容易对数据库进行拆分,做到高可用,易拓展,解耦。<sup data-text="《高性能MySQL》" data-url="" data-draft-node="inline" data-draft-type="reference" data-numero="2">[2]</sup> 但是子查询另一个很明显的问题就是效率比较低,比如创建临时表和查询时重复扫表。所以我们可以根据不同的嵌套查询类型进行查询优化。</p><p data-pid="fftusaM9"><b>嵌套查询的几种主要类型</b></p><p data-pid="plRtUXfK">只列举几种常见类型,更多类型可以参考论文<sup data-text="Kim, Won. "On optimizing an SQL-like nested query." ACM Transactions on Database Systems (TODS) 7.3 (1982): 443-469." data-url="https://dl.acm.org/doi/pdf/10.1145/319732.319745" data-draft-node="inline" data-draft-type="reference" data-numero="3">[3]</sup></p><p data-pid="8Ge6nUx7">1.类型A:子查询返回的值是一个聚集函数的结果,并且与外查询独立,即子查询不使用外查询表中的字段。例如</p><div class="highlight"><pre><code class="language-text">SELECT *
FROM exam e
WHERE e.grade=(SELECT max(grade) FROM exams)</code></pre></div><p data-pid="plz3ImI6">这里查询执行时先遍历exam表,过程中exam表的每个元组都要再遍历得出最高的分数,即重复遍历表和计算。</p><p data-pid="Fn1wcWjI">由于子查询没有用到外查询的表exam, 所以可以单独估算子查询的开销,即看作一个常数。</p><p data-pid="FXSmMnEJ">所以可以直接定义该子查询为一个常量,如下:</p><div class="highlight"><pre><code class="language-text">DEFINE m=(SELECT max(grade) FROM exams)
SELECT *
FROM exam e
WHERE e.grade=m</code></pre></div><p data-pid="aI0OtN_h">2.类型N:子查询不包含与外查询的任何连接操作,并且没有聚集函数。例如:</p><div class="highlight"><pre><code class="language-text">SELECT *
FROM exam e
WHERE e.department IN (SELECT f.id
FROM department f
WHERE f.name="computer science")</code></pre></div><p data-pid="CoF8p0ZD">这种类型的优化可以通过使用连接操作,如下:</p><div class="highlight"><pre><code class="language-text">SELECT *
FROM exam e,department f
WHERE e.department=f.id AND f.name="computer science")</code></pre></div><p data-pid="5G7Naov4">3.类型J:子查询依赖于外部查询的表,但是子查询没有聚集函数。如下:</p><div class="highlight"><pre><code class="language-text">SELECT OrderNr
FROM Orders O
WHERE ProdNr IN (SELECT ProdNr
FROM Shipping S
WHERE S.ShipNr=O.OrderNr
AND S.Date=current date)</code></pre></div><p data-pid="lNCmv7HK">对于Orders表中的每个元组来说,都要检索一次Shipping表,效率非常低下。可以转换成连接操作:</p><div class="highlight"><pre><code class="language-text">SELECT OrderNr
FROM Orders O,Shipping S
WHERE O.ProdNr=S.ProdNr
AND S.ShipNr=O.OrderNr
AND S.Date=current date</code></pre></div><p data-pid="ruMMe0_R">4.依赖连接dependent join<sup data-text="Neumann, Thomas, and Alfons Kemper. "Unnesting arbitrary queries." Datenbanksysteme für Business, Technologie und Web (BTW 2015) (2015)." data-url="https://dl.gi.de/bitstream/handle/20.500.12116/2418/383.pdf?sequence=1" data-draft-node="inline" data-draft-type="reference" data-numero="4">[4]</sup></p><p data-pid="r6zmMtHm">对于普通的连接操作,其公式为</p><figure data-size="normal"><img src="https://pic4.zhimg.com/v2-69ccb3c4a0f3ca2bca25348829534f97_r.jpg" data-caption="" data-size="normal" data-rawwidth="470" data-rawheight="94" class="origin_image zh-lightbox-thumb" width="470" data-original="https://pic4.zhimg.com/v2-69ccb3c4a0f3ca2bca25348829534f97_r.jpg"/></figure><p data-pid="gxO3llpU">而在嵌套查询中,还会出现依赖连接,即右表要先从左表中生成或引用左表,再与左表连接,公式为:</p><figure data-size="normal"><img src="https://pic1.zhimg.com/v2-df0901ff66323423ede3896928d1ff54_r.jpg" data-caption="" data-size="normal" data-rawwidth="952" data-rawheight="88" class="origin_image zh-lightbox-thumb" width="952" data-original="https://pic1.zhimg.com/v2-df0901ff66323423ede3896928d1ff54_r.jpg"/></figure><p data-pid="LxGU7C1W">假设查询每个公司的最贵的产品,并返回产品和公司名:</p><div class="highlight"><pre><code class="language-text">SELECT DISTINCT c.cname, px.pname
FROM company c, product px
WHERE c.cid=px.cid
and px.price=(SELECT max(p.price)
FROM product p
WHERE c.cid=p.cid);</code></pre></div><p data-pid="nTPcdPkj">生成的查询计划如下</p><figure data-size="normal"><img src="https://pic2.zhimg.com/v2-0c75714960ffbe05d3709ea4aa48d255_r.jpg" data-caption="" data-size="normal" data-rawwidth="1167" data-rawheight="900" class="origin_image zh-lightbox-thumb" width="1167" data-original="https://pic2.zhimg.com/v2-0c75714960ffbe05d3709ea4aa48d255_r.jpg"/></figure><p data-pid="T6QN7yle">这里对product使用了聚集函数后又再与两个表连接再使用聚集函数,是一种nest loop join,进行了大量重复计算。所以可以直接将子查询放到From语句中,与另外两个表直接连接,这样查询计划会都使用更加高效的hash join的方式生成连接表再聚集。</p>
<p data-pid="0X3xP-dK">在物理优化阶段主要解决的问题:</p><ul><li data-pid="5Dli8etC">选择最优的单表扫描方式</li><li data-pid="1xC5OqN4">如何最优的连接两个表?</li><li data-pid="YQw2Mz9U">选择最优的多个表的连接顺序</li><li data-pid="8FO-1470">是否要对多个表链接的每种连接顺序都探索?怎么在不探索全部组合时找到最优?</li></ul><p data-pid="4rNSA0SV">基于代价的查询优化方式对查询执行计划做了定量的分析,对每一个可能的执行方式进行评估,挑出代价最小的作为最优的计划。</p><p data-pid="0_Qg6OW9">查询代价估算的重点是代价估算模型,是物理查询优化的依据。此外,选择率也是很重要的一个概念,对代价求解起着重要作用。</p><p data-pid="KWkR9Yac">查询代价估算基于CPU代价和IO代价,计算公式表示:</p><p data-pid="KCSWktE8">总代价=IO 代价 + CPU 代价<br/>COST=P * a_page_cpu_time + W * T</p><p data-pid="bJccTkP_">其中:</p><ul><li data-pid="_vOMPZQO">P为计划运行时访问的页面数,a_page_cpu_time是每个页面读取的CPU时间花费,其乘积反映了IO代价。</li><li data-pid="2rwRS8jF">T为访问的元组数,反映了CPU花费(存储层是以页面为单位,数据以页面的形式被读入内存,每个页面上可能有多条元组,访问元组需要解析元组结构,才能把元组上的字段读出,这消耗的是CPU)。如果是索引扫描,则还会包括索引读取的花费。</li><li data-pid="Hn45BEjQ">W为权重因子,表明IO到CPU的相关性,又称选择率(selectivity)。</li></ul><p data-pid="6oog2UdZ">选择率的精确程度直接影响最优计划的选取,其常用计算方法如下:</p><ul><li data-pid="Ub-faNnw"><b>无参数法</b>(Non-Parametric Method)。使用adhoc数据结构或直方图维护属性值的分布,最常用直方图。</li><li data-pid="2w56CDON"><b>参数法</b>(Parametric Method)。使用具有一些自由统计参数(参数是预先估计出来的)的数学分布函数逼近真实分布。</li><li data-pid="TMd3_Rw1"><b>曲线拟合法</b>(Curve Fitting)。为克服参数法的不灵活性,用一般多项式和标准最小方差来逼近属性值的分布。</li><li data-pid="I1_PUkQW"><b>抽样法</b>(sampling)。从数据库中抽取部分样本元组,针对这些样本进行查询,然后收集统计数据,只有足够的样本被测试之后,才能达到预期的精度。</li><li data-pid="Y59eXAHq"><b>综合法</b>。将以上几种方法结合起来,如抽样法和直方图法结合。</li></ul><p data-pid="Rev-oHbv">单表扫描需要从表上获取元组,直接关联到物理IO的读取,所以不同的单表扫描方式,有不同的代价。</p><p data-pid="5g_WZAKE">单表扫描是完成表连接的基础。获取单表数据的方式:</p><ul><li data-pid="qlkysM6Z">全表扫描表数据。</li><li data-pid="Zf2_sg-y">局部扫描表数据。</li></ul><p data-pid="01yb3hO4">全表扫描通常采取顺序读取的算法。单表扫描和IO操作密切相关,所以很多算法重点在IO上。<br/><br/>常用的单表扫描算法:</p><ul><li data-pid="GLHMeTQJ"><b>顺序扫描</b>(SeqScan)。从物理存储上按照存储顺序直接读取表的数据;其效果受有数据量影响。</li><li data-pid="EFWxKLw4"><b>索引扫描</b>(IndexScan)。根据索引键读索引,找出物理元组的位置,再读取数据页且有序;选择率越低,越适宜使用索引扫描,读数据花费的IO越少。</li><li data-pid="a99Q4O7P"><b>只读索引扫描</b>(IndexOnlyScan)。根据索引键读索引,索引中的数据就已满足条件判断,少了读取数据的IO花费。</li><li data-pid="T15Ghs1E"><b>行扫描</b>(RowIdScan)。直接定位表中的某一行。通常为元组增加特殊的列,可以直接计算出元组的物理位置,然后直接读取数量页面。PostgreSQL中的Tid扫描就是在元组头上增加名为CTID的列,用来直接计算元组的物理位置。</li><li data-pid="muoifrW1"><b>并行表扫描</b>(ParallelTableScan)。并行通过顺序的方式获取同一个表的数据。</li><li data-pid="PXX3XZE3"><b>并行索引扫描</b>(ParallelIndexScan)。并行通过索引的方式获取同一个表的数据。</li><li data-pid="qRBe85J8"><b>组合多个索引扫描</b>(MultipleIndexScan)。对同一个元组的组合条件(涉及多个索引)进行多次索引扫描,然后在内存中用位图描述索引扫描结果中符合索引条件的元组位置。本质上不是单表的扫描方式,是构建在单表的多个索引扫描基础上的。</li></ul><p data-pid="FpiWp7Pg">对于局部扫描,通常采用索引,实现少量数据的读取优化。这是一种随机读取数据的方式。有的系统对于随机读做了优化,即把要读取的数据的物理位置排序,然后一批读入,保障了磁盘单次扫描获取尽可能多的数据,提高了IO效率。<br/><br/>在并行操作的时候,可能因不同隔离级别的要求,需要解决数据一致性的问题。因为索引扫描只在满足条件的元组上加锁,所以索引扫描在多用户环境中可能会比顺序扫描效率高。查询优化器这里倾向于选择索引扫描,这是一条启发式优化规则。</p><ul><li data-pid="zA2yKw4d">单表扫描,需要考虑IO的花费。</li><li data-pid="ROUII5O7">顺序扫描,主要是IO花费加上元组从页面中解析的花费。</li><li data-pid="NbCn8TnA">索引扫描和其他方式的扫描,需要考虑选择率的问题。</li></ul><p data-pid="HGqLlErr">单表扫描操作的代价估算公式(表3-1):</p><figure data-size="normal"><img src="https://pic3.zhimg.com/v2-cd58ba755d7e9707b51204584c05f38a_r.jpg" data-caption="" data-size="normal" data-rawwidth="1126" data-rawheight="294" class="origin_image zh-lightbox-thumb" width="1126" data-original="https://pic3.zhimg.com/v2-cd58ba755d7e9707b51204584c05f38a_r.jpg"/></figure><ul><li data-pid="H9NxvZa3">a_page_IO_time,一个页面的IO花费。</li><li data-pid="B8B28nSY">N_page,数据页面数。</li><li data-pid="b0ipB9te">N_page_index,索引页面数。</li><li data-pid="5My-_27g">a_tuple_CPU_time,一个元组从页面中解析的CPU花费。</li><li data-pid="_pGbLyD5">N_tuple,元组数。</li><li data-pid="XtvW6yRf">C_index,索引的IO花费,C_index=N_page_index * a_page_IO_time。</li><li data-pid="DzXHx1E4">N_tuple_index,索引作用下的可用元组数,N_tuple_index=N_tuple×索引选择率。</li><li data-pid="YvTHrEi2">a_tuple_IO_time,一个元组的IO花费。</li></ul><p data-pid="_WJiy5-u">索引是建立在表上的,本质上是通过索引直接定位表的物理元组,加快数据的读取。</p><p data-pid="1gVIoefu">索引是提高查询效率的有效手段。通常查询优化器使用索引的原则如下:</p><ul><li data-pid="-4Nji-2m">索引列作为条件出现在WHERE、HAVING、ON子句中,这样有利于利用索引过滤元组。</li><li data-pid="DSnOlgZC">索引列是被连接的表(内表)对象的列且存在于连接条件中。</li><li data-pid="v7AG6xyk">特殊情况,如排序操作、在索引列上求MIN、MAX值等。</li></ul><p data-pid="M48ThSeO">索引可用的条件如下:</p><ul><li data-pid="vG2yqBwN">在WHERE、JOIN/ON、HAVING的条件中出现“key <op> 常量”格式的条件子句(索引列不能参与带有变量的表达式的运算)。</li><li data-pid="Rz2fviUd">操作符不能是<>操作符(不等于操作符在任何类型的列上不能使用索引,此时顺序扫描的效果通常好于索引扫描)。</li><li data-pid="buj1N0x6">索引列的值选择率越低,索引越有效,通常认为选择率小于0.1则索引扫描效果会更好。</li></ul><p data-pid="ZxqPiu7l">创建如下表,便于后面示例说明:</p><div class="highlight"><pre><code class="language-sql"><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">A</span> <span class="p">(</span>
<span class="n">a1</span> <span class="nb">INT</span> <span class="k">UNIQUE</span><span class="p">,</span>
<span class="n">a2</span> <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">9</span><span class="p">),</span>
<span class="n">a3</span> <span class="nb">INT</span>
<span class="p">);</span> <span class="c1">-- 注:表会建隐含索引"a_a1_key"</span></code></pre></div><p data-pid="NWokblBb"><b>1. 对目标列、WHERE等条件子句的影响</b></p><p data-pid="U_FLOOr7">索引列出现在目标列,通常不可使用索引,查询执行计划只能使用顺序扫描,对查询语句的优化没有好的影响。但聚集函数MIN/MAX用在索引列上,出现在目标列,可使用索引。</p><div class="highlight"><pre><code class="language-sql"><span class="k">SELECT</span> <span class="n">A</span><span class="p">.</span><span class="n">a1</span> <span class="k">FROM</span> <span class="n">A</span><span class="p">;</span> <span class="c1">-- 顺序扫描,不可使用索引
</span><span class="c1"></span><span class="k">SELECT</span> <span class="k">MAX</span><span class="p">(</span><span class="n">A</span><span class="p">.</span><span class="n">a1</span><span class="p">)</span> <span class="k">FROM</span> <span class="n">A</span><span class="p">;</span> <span class="c1">-- 可使用索引</span></code></pre></div><p data-pid="mP0DOcZk">索引列出现在WHERE子句中,可使用索引。这个好理解。</p><div class="highlight"><pre><code class="language-sql"><span class="k">SELECT</span> <span class="n">A</span><span class="p">.</span><span class="n">a1</span> <span class="k">FROM</span> <span class="n">A</span> <span class="k">WHERE</span> <span class="n">A</span><span class="p">.</span><span class="n">a1</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span></code></pre></div><p data-pid="rQwUyKax">索引列出现在JOIN/ON子句中,作为连接条件,不可使用索引。<br/>索引列出现在JOIN/ON子句中,作为限制条件满足“key <op> 常量”格式可用索引。</p><div class="highlight"><pre><code class="language-sql"><span class="k">SELECT</span> <span class="n">A</span><span class="p">.</span><span class="o">*</span><span class="p">,</span> <span class="n">B</span><span class="o">*</span> <span class="k">FROM</span> <span class="n">A</span> <span class="k">JOIN</span> <span class="n">B</span> <span class="k">ON</span> <span class="p">(</span><span class="n">a1</span><span class="o">=</span><span class="n">b1</span><span class="p">);</span> <span class="c1">-- 不可用索引
</span><span class="c1"></span><span class="k">SELECT</span> <span class="n">A</span><span class="p">.</span><span class="o">*</span><span class="p">,</span> <span class="n">B</span><span class="o">*</span> <span class="k">FROM</span> <span class="n">A</span> <span class="k">JOIN</span> <span class="n">B</span> <span class="k">ON</span> <span class="p">(</span><span class="n">a1</span><span class="o">=</span><span class="n">b1</span><span class="p">)</span> <span class="k">AND</span> <span class="n">A</span><span class="p">.</span><span class="n">a1</span><span class="o">=</span><span class="mi">1</span><span class="p">;</span> <span class="c1">-- 可用索引,查询优化器根据“常量传递”优化技术推知b1=1,所以表A和表B可各自使用索引扫描。</span></code></pre></div><p data-pid="Sl0ZA5IH">索引列出现在连接的WHERE子句中,可用索引,但与子查询比较,格式上不满足“key <op> 常量”,不可用索引。</p><div class="highlight"><pre><code class="language-sql"><span class="k">SELECT</span> <span class="n">A</span><span class="p">.</span><span class="o">*</span><span class="p">,</span> <span class="n">B</span><span class="o">*</span> <span class="k">FROM</span> <span class="n">A</span> <span class="k">JOIN</span> <span class="n">B</span> <span class="k">ON</span> <span class="p">(</span><span class="n">a1</span><span class="o">=</span><span class="n">b1</span><span class="p">)</span> <span class="k">WHERE</span> <span class="n">A</span><span class="p">.</span><span class="n">a1</span><span class="o">=</span><span class="mi">1</span><span class="p">;</span> <span class="c1">-- 可用索引,查询优化器根据“常量传递”优化技术推知b1=1,所以表A和表B可各自使用索引扫描。
</span><span class="c1"></span><span class="k">SELECT</span> <span class="n">E</span><span class="p">.</span><span class="n">e1</span> <span class="k">FROM</span> <span class="n">E</span> <span class="k">WHERE</span> <span class="n">E</span><span class="p">.</span><span class="n">e1</span> <span class="k">IN</span> <span class="p">(</span><span class="k">SELECT</span> <span class="n">A</span><span class="p">.</span><span class="n">a1</span> <span class="k">FROM</span> <span class="n">A</span><span class="p">);</span> <span class="c1">-- 不可用索引</span></code></pre></div><p data-pid="YntMbJ2o"><b>2.对GROUPBY子句的影响</b></p><p data-pid="Hkz_JQsq">索引列出现在GROUPBY子句中,不触发索引扫描。<br/>WHERE子句出现索引列,且GROUPBY子句出现索引列,索引扫描被使用。<br/>WHERE子句中出现非索引列,且GROUPBY子句出现索引列,索引扫描不被使用。</p><div class="highlight"><pre><code class="language-sql"><span class="k">SELECT</span> <span class="n">A</span><span class="p">.</span><span class="n">a1</span> <span class="k">FROM</span> <span class="n">A</span> <span class="k">GROUP</span> <span class="k">BY</span> <span class="n">a1</span><span class="p">;</span> <span class="c1">-- 顺序扫描,不可使用索引
</span><span class="c1"></span><span class="k">SELECT</span> <span class="n">A</span><span class="p">.</span><span class="n">a1</span> <span class="k">FROM</span> <span class="n">A</span> <span class="k">WHERE</span> <span class="n">a1</span> <span class="o">></span> <span class="mi">2</span> <span class="k">GROUP</span> <span class="k">BY</span> <span class="n">a1</span><span class="p">;</span> <span class="c1">-- WHERE子句中的索引列使用符合“key <op> 常量”的格式,可使用索引
</span><span class="c1"></span><span class="k">SELECT</span> <span class="n">A</span><span class="p">.</span><span class="n">a1</span> <span class="k">FROM</span> <span class="n">A</span> <span class="k">WHERE</span> <span class="n">a3</span> <span class="o">></span> <span class="mi">2</span> <span class="k">GROUP</span> <span class="k">BY</span> <span class="n">a1</span><span class="p">;</span> <span class="c1">-- 不可使用索引</span></code></pre></div><p data-pid="O1H36T_w"><b>3.对HAVING子句的影响</b></p><p data-pid="TrSWms28">索引列出现在HAVING子句中与出现在WHERE子句中类似。<br/>WHERE子句中出现非索引列,且GROUPBY和HAVING子句出现索引列,索引扫描被使用。</p><div class="highlight"><pre><code class="language-sql"><span class="k">SELECT</span> <span class="n">A</span><span class="p">.</span><span class="n">a1</span> <span class="k">FROM</span> <span class="n">A</span> <span class="k">WHERE</span> <span class="n">a3</span><span class="o">></span><span class="mi">2</span> <span class="k">GROUP</span> <span class="k">BY</span> <span class="n">a1</span> <span class="k">HAVING</span> <span class="n">a1</span><span class="o">></span><span class="mi">2</span><span class="p">;</span></code></pre></div><p data-pid="ihpiFeut">尽管WHERE子句使用了非索引列a3,但HAVING子句使用了索引列a1,且a1>2表达式符合“key <op> 常量”的格式,所以会使用位图索引扫描。<br/><br/><b>4.对ORDERBY子句的影响</b></p><p data-pid="6jAFT24u">ORDERBY子句中出现索引列可使用索引扫描,出现非索引列不可使用索引扫描。</p><div class="highlight"><pre><code class="language-sql"><span class="k">SELECT</span> <span class="n">A</span><span class="p">.</span><span class="o">*</span> <span class="k">FROM</span> <span class="n">A</span> <span class="k">ORDER</span> <span class="k">BY</span> <span class="n">a1</span><span class="p">;</span> <span class="c1">-- 可使用索引扫描
</span><span class="c1"></span><span class="k">SELECT</span> <span class="n">A</span><span class="p">.</span><span class="o">*</span> <span class="k">FROM</span> <span class="n">A</span> <span class="k">ORDER</span> <span class="k">BY</span> <span class="n">a3</span><span class="p">;</span> <span class="c1">-- 不可使用索引扫描</span></code></pre></div><p data-pid="9BdpHNPu"><b>5.对DISTINCT的影响</b></p><p data-pid="-BM6hDCk">DISTINCT子句管辖范围内出现索引列,不可使用索引扫描。<br/>DISTINCT子句管辖范围内出现索引列,因WHERE子句内使用索引列,故其可使用索引扫描,但这和DISTINCT操作没有关系。</p><div class="highlight"><pre><code class="language-sql"><span class="k">SELECT</span> <span class="k">DISTINCT</span> <span class="n">A</span><span class="p">.</span><span class="n">a1</span> <span class="k">FROM</span> <span class="n">A</span><span class="p">;</span> <span class="c1">-- 不可使用索引扫描
</span><span class="c1"></span><span class="k">SELECT</span> <span class="k">DISTINCT</span> <span class="n">A</span><span class="p">.</span><span class="n">a1</span> <span class="k">FROM</span> <span class="n">A</span> <span class="k">WHERE</span> <span class="n">a1</span> <span class="o">></span> <span class="mi">2</span><span class="p">;</span> <span class="c1">-- 可使用索引扫描</span></code></pre></div><p data-pid="CBcPOq2U">首先创建表:</p><div class="highlight"><pre><code class="language-sql"><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">E</span> <span class="p">(</span>
<span class="n">e1</span> <span class="nb">INT</span><span class="p">,</span>
<span class="n">e2</span> <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">9</span><span class="p">),</span>
<span class="n">e3</span> <span class="nb">INT</span><span class="p">,</span>
<span class="k">PRIMARY</span> <span class="k">KEY</span><span class="p">(</span><span class="n">e1</span><span class="p">,</span> <span class="n">e3</span><span class="p">)</span>
<span class="p">);</span> <span class="c1">-- 注:表会创建隐含索引"e_pkey"</span></code></pre></div><ul><li data-pid="trZdyGs0">使用联合索引的全部索引键,可触发索引的使用。</li><li data-pid="dop9r6kJ">使用联合索引的前缀部分索引键,如“key_part_1 <op> 常量”,可触发索引的使用。</li><li data-pid="W3wvUS2r">使用非前缀部分索引键,如“key_part_2 <op> 常量”,不可触发索引的使用。</li><li data-pid="4BT6BaDG">使用联合索引的全部索引键,但索引键不是AND操作,不可触发索引的使用。</li></ul><div class="highlight"><pre><code class="language-sql"><span class="k">SELECT</span> <span class="n">E</span><span class="p">.</span><span class="o">*</span> <span class="k">FROM</span> <span class="n">E</span> <span class="k">WHERE</span> <span class="n">E</span><span class="p">.</span><span class="n">e1</span><span class="o">=</span><span class="mi">1</span> <span class="k">AND</span> <span class="n">E</span><span class="p">.</span><span class="n">e3</span><span class="o">=</span><span class="mi">2</span><span class="p">;</span> <span class="c1">-- 全部索引,使用索引
</span><span class="c1"></span><span class="k">SELECT</span> <span class="n">E</span><span class="p">.</span><span class="o">*</span> <span class="k">FROM</span> <span class="n">E</span> <span class="k">WHERE</span> <span class="n">E</span><span class="p">.</span><span class="n">e1</span><span class="o">=</span><span class="mi">1</span><span class="p">;</span> <span class="c1">-- 前缀部分索引,使用索引
</span><span class="c1"></span><span class="k">SELECT</span> <span class="n">E</span><span class="p">.</span><span class="o">*</span> <span class="k">FROM</span> <span class="n">E</span> <span class="k">WHERE</span> <span class="n">E</span><span class="p">.</span><span class="n">e3</span><span class="o">=</span><span class="mi">2</span><span class="p">;</span> <span class="c1">-- 非前缀部分索引,不使用索引
</span><span class="c1"></span><span class="k">SELECT</span> <span class="n">E</span><span class="p">.</span><span class="o">*</span> <span class="k">FROM</span> <span class="n">E</span> <span class="k">WHERE</span> <span class="n">E</span><span class="p">.</span><span class="n">e1</span><span class="o">=</span><span class="mi">1</span> <span class="k">OR</span> <span class="n">E</span><span class="p">.</span><span class="n">e3</span><span class="o">=</span><span class="mi">2</span><span class="p">;</span> <span class="c1">-- 全部索引键,但非AND操作,不使用索引</span></code></pre></div><p data-pid="Ecff6Bt9">首先创建表:</p><div class="highlight"><pre><code class="language-sql"><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">F</span> <span class="p">(</span>
<span class="n">f1</span> <span class="nb">INT</span> <span class="k">UNIQUE</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
<span class="n">f2</span> <span class="nb">INT</span> <span class="k">UNIQUE</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
<span class="n">f3</span> <span class="nb">INT</span><span class="p">,</span>
<span class="k">PRIMARY</span> <span class="k">KEY</span><span class="p">(</span><span class="n">f1</span><span class="p">,</span> <span class="n">f3</span><span class="p">)</span>
<span class="p">);</span> <span class="c1">-- 注:表会创建隐含索引f_pkey、f_f1_key、f_f2_key</span></code></pre></div><p data-pid="MyVCR8TQ">WHERE条件子句出现两个可利用的索引,优选最简单的索引。</p><div class="highlight"><pre><code class="language-sql"><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">F</span> <span class="k">WHERE</span> <span class="n">f1</span><span class="o">=</span><span class="mi">2</span> <span class="k">AND</span> <span class="n">f2</span><span class="o">=</span><span class="mi">1</span><span class="p">;</span> <span class="c1">-- 因为f1列上存在两个索引,比f2列上的索引复杂,所以会优选f2的索引。本质是取决于代价估算模型的评估。
</span><span class="c1"></span><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">F</span> <span class="k">WHERE</span> <span class="n">f1</span><span class="o">=</span><span class="mi">1</span> <span class="k">AND</span> <span class="p">(</span><span class="n">f1</span><span class="o">=</span><span class="mi">1</span> <span class="k">AND</span> <span class="n">f3</span><span class="o">=</span><span class="mi">3</span><span class="p">);</span> <span class="c1">-- 索引重叠时,选取的是f1列上的独立索引而不是联合索引
</span><span class="c1"></span><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">F</span> <span class="k">WHERE</span> <span class="p">(</span><span class="n">f1</span><span class="o">=</span><span class="mi">2</span> <span class="k">AND</span> <span class="n">f3</span><span class="o">=</span><span class="mi">3</span><span class="p">)</span> <span class="k">AND</span> <span class="n">f2</span><span class="o">=</span><span class="mi">1</span><span class="p">;</span> <span class="c1">-- 选取的是f2列上的独立索引而不是f1和f3构成的联合索引
</span><span class="c1"></span><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">F</span> <span class="k">WHERE</span> <span class="n">f2</span><span class="o">></span><span class="mi">1</span> <span class="k">AND</span> <span class="n">f2</span><span class="o"><</span><span class="mi">100</span> <span class="k">AND</span> <span class="n">f1</span><span class="o">=</span><span class="mi">3</span><span class="p">;</span> <span class="c1">-- 范围扫描选择率比等值比较的选择率大,所以查询优化器选择了f1上的索引
</span><span class="c1"></span><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">F</span> <span class="k">WHERE</span> <span class="n">f2</span><span class="o">></span><span class="mi">1</span> <span class="k">AND</span> <span class="n">f2</span><span class="o"><</span><span class="mi">100</span> <span class="k">AND</span> <span class="n">f3</span><span class="o">=</span><span class="mi">3</span><span class="p">;</span> <span class="c1">-- 由于f3不是索引键的前缀部分,所以会选择f2上的索引</span></code></pre></div><p data-pid="WvPkFyEu">连接运算是关系代数的一项重要操作,多个表连接建立在两表之间连接的基础上。研究两表连接的方式,对连接效率的提高有着直接的影响。</p><p data-pid="Obsf4NVz"><b>1.嵌套循环连接算法</b></p><p data-pid="_0pbhmNp">嵌套循环连接算法是两表做连接采用的最基本算法。算法描述:</p><div class="highlight"><pre><code class="language-sql"><span class="k">FOR</span> <span class="k">EACH</span> <span class="k">ROW</span> <span class="n">rl</span> <span class="k">IN</span> <span class="n">t1</span> <span class="err">{</span>
<span class="k">FOR</span> <span class="k">EACH</span> <span class="k">ROW</span> <span class="n">r2</span> <span class="k">IN</span> <span class="n">t2</span> <span class="err">{</span>
<span class="k">IF</span> <span class="n">r1</span><span class="p">,</span><span class="n">r2</span> <span class="n">SATISFIES</span> <span class="k">JOIN</span> <span class="n">CONDITIONS</span>
<span class="k">JOIN</span> <span class="n">r1</span><span class="p">,</span><span class="n">r2</span>
<span class="err">}</span>
<span class="err">}</span></code></pre></div><p data-pid="Po9f4SfS">数据库引擎在实现该算法的时候,以元组为单位进行连接。元组从一个内存页面获取来,而内存页面通过IO操作获得,每个IO申请以“块”为单位尽量读入多个页面。可以对该算法进行改进,改进后的算法称为基于块的嵌套循环连接算法。算法描述:</p><div class="highlight"><pre><code class="language-sql"><span class="k">FOR</span> <span class="k">EACH</span> <span class="n">CHUNK</span> <span class="n">c1</span> <span class="k">OF</span> <span class="n">t1</span> <span class="err">{</span>
<span class="k">IF</span> <span class="n">c1</span> <span class="k">NOT</span> <span class="k">IN</span> <span class="n">MEMORY</span> <span class="o">//</span><span class="err">系统一次读入多个页面,所以不需要每次消耗</span><span class="n">IO</span>
<span class="k">READ</span> <span class="n">CHUNK</span> <span class="n">c1</span> <span class="k">INTO</span> <span class="n">MEMORY</span>
<span class="k">FOR</span> <span class="k">EACH</span> <span class="k">ROW</span> <span class="n">rl</span> <span class="k">IN</span> <span class="n">CHUNK</span> <span class="n">c1</span> <span class="err">{</span><span class="o">//</span><span class="err">从页面中分析出元组,消耗</span><span class="n">CPU</span>
<span class="k">FOR</span> <span class="k">EACH</span> <span class="n">CHUNK</span> <span class="n">c2</span> <span class="k">OF</span> <span class="n">t2</span> <span class="err">{</span>
<span class="k">IF</span> <span class="n">c2</span> <span class="k">NOT</span> <span class="k">IN</span> <span class="n">MEMORY</span>
<span class="k">READ</span> <span class="n">CHUNK</span> <span class="n">c2</span> <span class="k">INTO</span> <span class="n">MEMORY</span>
<span class="k">FOR</span> <span class="k">EACH</span> <span class="k">ROW</span> <span class="n">r2</span> <span class="k">IN</span> <span class="n">c2</span> <span class="err">{</span> <span class="o">//</span><span class="err">从页面中分析出元组,消耗</span><span class="n">CPU</span>
<span class="k">IF</span> <span class="n">r1</span><span class="p">,</span><span class="n">r2</span> <span class="n">SATISFIES</span> <span class="k">JOIN</span> <span class="n">CONDITIONS</span>
<span class="k">JOIN</span> <span class="n">r1</span><span class="p">,</span><span class="n">r2</span>
<span class="err">}</span>
<span class="err">}</span>
<span class="err">}</span>
<span class="err">}</span></code></pre></div><p data-pid="3K24e5b4">其他一些两表连接算法,多是在此基础上进行的改进。如基于索引做改进,在考虑了聚簇和非聚簇索引的情况下,如果内表有索引可用,则可以加快连接操作的速度。另外,如果内层循环的最后一个块使用后作为下次循环的第一个块,则可以节约一次IO。如果外层元组较少,内层的元组驻留内存多一些,则能有效提高连接的效率。<br/><br/>嵌套循环连接算法和基于块的嵌套循环连接算法适用于内连接、左外连接、半连接、反半连接等语义的处理。</p><p data-pid="XJzyB6AY"><b>2.排序归并连接算法</b></p><p data-pid="r4nXusqp">简称归并连接算法。算法步骤:</p><ol><li data-pid="Jh4Q33vO">为两个表创建可用内存缓冲区数为M的M个子表</li><li data-pid="vELdoyg1">将每个子表排序</li><li data-pid="LQVJsQPm">从读入每个子表的第一块到M个块中,找出其中最小的先进行两个表的元组的匹配,找出次小的匹配……依此类推</li><li data-pid="VhTvJaGQ">完成其他子表的两表连接。</li></ol><p data-pid="U5FiiSEl">归并连接算法要求内外表都有序,所以对于内外表都要排序。如果有索引,可以利用索引进行排序。<br/>归并连接算法适用于内连接、左外连接、右外连接、全外连接、半连接、反半连接等语义的处理。</p><p data-pid="TAkDV7pI"><b>3.Hash连接算法</b></p><p data-pid="T017AzDV">基于Hash的两表连接常见的算法:</p><ul><li data-pid="A1CA7tGp"><b>简单Hash连接</b>(Simple Hash Join,SHJ)算法,用连接列作为Hash的关键字,对内表建立Hash表,然后对外表的每个元组的连接列用Hash函数求值,值映射到内表的Hash表就可以连接了;否则,探索外表的下一个元组。</li><li data-pid="k2zISLr7"><b>优美Hash连接</b>(GraceHash Join,GHJ)算法,改进SHJ算法,把内表和外表划分成等大小的子表,然后对外表和内表的每个相同下标值的子表进行SHJ算法的操作,可以避免因内存小反复读入内外表的数据的问题。</li><li data-pid="piuUxODr"><b>混合Hash连接</b>(Hybrid Hash Join,HHJ)算法,结合SHJ和GHJ算法的优点,把第一个子表保存到内存不刷出,如果内存很大,则子表能容纳更大量的数据,效率接近于SHJ。</li></ul><p data-pid="MZZcd9dP">Hash算法存在以下限制和问题:</p><ul><li data-pid="9gC1Fw_T">Hash连接算法只适用于数据类型相同的等值连接。</li><li data-pid="kWO0o8VV">对内存要求较大。</li><li data-pid="tHLLrAu2">如果表中连接列值重复率很高,Hash连接算法就效率不高。</li><li data-pid="UOYB4d8r">存在“分区溢出”问题。当内存小或数据倾斜(分布不均衡)时,通过把一个表划分为多个子表仍不能消除Hash冲突的问题,如GHJ算法。</li><li data-pid="qJUEuA4T">要求内表不能太大,如果超出Hash表申请的内存大小且不能继续动态申请,则需要写临时文件,会导致IO的颠簸(PostgreSQL存在此类问题)。</li></ul><p data-pid="ri6bxVW7">Hash连接算法适用于内连接、左外连接、右外连接、全外连接、半连接、反半连接等语义的处理。</p><p data-pid="n9IbgpFu">从内存的容量角度看,两表连接算法可以分为:</p><ul><li data-pid="I_KUcXsg">一趟算法</li><li data-pid="0MNc8UCg">两趟算法</li><li data-pid="NIfzzliZ">多趟算法</li></ul><p data-pid="zcFAPpXD">所谓“趟”是指从存储系统获取全部数据的次数。一趟算法因内存空间能容纳下全部数据,所以读取一次即可。两趟算法的第一趟从存储系统获取两表的数据,如做排序等处理后,再写入外存的临时文件;第二趟重新读入临时文件进行进一步处理。多趟算法的思想和两趟算法基本相同,用以处理更大量的数据。<br/><br/>连接算法和索引及趟数的关系表(表3-2):</p><figure data-size="normal"><img src="https://pic2.zhimg.com/v2-6daaf4777be9ad3b72496cb27e25acad_r.jpg" data-caption="" data-size="normal" data-rawwidth="1500" data-rawheight="559" class="origin_image zh-lightbox-thumb" width="1500" data-original="https://pic2.zhimg.com/v2-6daaf4777be9ad3b72496cb27e25acad_r.jpg"/></figure><p data-pid="_jUr-jzS">两表连接算法的代价表(表3-3):</p><figure data-size="normal"><img src="https://pic1.zhimg.com/v2-850639a8125d0e0dd30e61bd3e18db2c_r.jpg" data-caption="" data-size="normal" data-rawwidth="1500" data-rawheight="328" class="origin_image zh-lightbox-thumb" width="1500" data-original="https://pic1.zhimg.com/v2-850639a8125d0e0dd30e61bd3e18db2c_r.jpg"/></figure><ul><li data-pid="XJrrbosH">a_tuple_cpu_time,获取一个元组消耗的CPU时间。</li><li data-pid="jxOINCrD">N-outer,扫描获取的外表元组数。</li><li data-pid="rIdzreaJ">N-inner,扫描获取的内表元组数,N-inner=N-inner-all×选择率,其中N-inner-all表示内表的所有元组数。</li><li data-pid="aNBMUNot">C-outer,扫描外表的代价,C-outer=N-outer×a_tuple_cpu_time。</li><li data-pid="kTQoDFb-">C-inner,扫描内表的代价,C-inner=N-inner×a_tuple_cpu_time。</li><li data-pid="tgJdARJb">C-inner-index,使用索引扫描内表的代价,通常C-inner-index会小于C-inner。</li><li data-pid="eDzKGxJ7">C-outersort,外表排序的代价。</li><li data-pid="E3EUXjl8">C-innersort,内表排序的代价。</li><li data-pid="5Ayrxz9M">C-createhash,创建Hash的代价。</li></ul><p data-pid="9NvG8nth">多表连接算法需要解决两个问题:</p><ul><li data-pid="ZcMdJNVV">多表连接的顺序:表的不同的连接顺序,会产生许多不同的连接路径;不同的连接路径有不同的效率。</li><li data-pid="A2NnT4uI">多表连接的搜索空间:因为多表连接的顺序不同,产生的连接组合会有多种,如果这个组合的数目巨大,连接次数会达到一个很高的数量级,最大可能的连接次数是N!(N的阶乘)。如何将搜索空间限制在一个可接受的时间范围内,并高效地生成查询执行计划将成为一个难点。</li></ul><p data-pid="-KR7GDi3">多表间的连接顺序表示了查询计划树的基本形态。一棵树就是一种查询路径,SQL的语义可以由多棵这样的树表达,从中选择花费最少的树,就是最优查询计划形成的过程。<br/><br/>一棵树包括左深连接树、右深连接树、紧密树。</p><figure data-size="normal"><img src="https://pic1.zhimg.com/v2-62b9f1975ebd985dc061c9e1892fd320_r.jpg" data-caption="" data-size="normal" data-rawwidth="1467" data-rawheight="556" class="origin_image zh-lightbox-thumb" width="1467" data-original="https://pic1.zhimg.com/v2-62b9f1975ebd985dc061c9e1892fd320_r.jpg"/></figure><p data-pid="f_-BykNB"><br/>不同的连接顺序,会生成不同大小的中间关系,对应CPU和IO消耗不同。PostgreSQL中会尝试多种连接方式存放到path上,以找出花费最小的路径。<br/><br/>人们针对树的形成及其花费代价最少的,提出了诸多算法。树形成过程有以下两种策略:</p><ul><li data-pid="cEkhC5YZ"><b>至顶向下</b>。从SQL表达式树的树根开始,向下进行,估计每个结点可能的执行方法,计算每种组合的代价,从中挑选最优的。</li><li data-pid="EgIiGOoj"><b>自底向上</b>。从SQL表达式树的树叶开始,向上进行,计算每个子表达式的所有实现方法的代价,从中挑选最优的,再和上层(靠近树根)的进行连接,周而复始直至树根。</li></ul><p data-pid="2p3q6hMV">在数据库实现中,多数数据库采取了自底向上的策略。</p><p data-pid="Iao_mTiu">动态规划,指决策依赖于当前状态,又随即引起状态的转移,一个决策序列就是在变化的状态中产生出来的。<br/><br/>“动态规划”将待求解的问题分解为若干个子问题(子阶段),按顺序求解子问题,前一子问题的解为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解<br/><br/>主要概念有:</p><ul><li data-pid="buB0Zj15"><b>阶段</b>。把求解问题的过程分成若干个相互联系的阶段,以便于求解。</li><li data-pid="bK9C3qUH"><b>状态</b>。表示每个阶段开始面临的自然状况或客观条件,它不以人们的主观意志为转移,也称为不可控因素。</li><li data-pid="oSKiJALy"><b>无后效性</b>。状态应该具有的性质,如果给定某一阶段的状态,则在这一阶段以后过程的发展不受这阶段以前各段状态的影响。</li><li data-pid="7wIrpn15"><b>决策</b>。一个阶段的状态确定后,从该状态演变到下一阶段某个状态的选择(行动)。</li><li data-pid="5D78fuzt"><b>策略</b>。由每个阶段的决策组成的序列称为策略。对于每一个实际的多阶段决策过程,可供选取的策略有一定的范围限制,这个范围称为允许策略集合。允许策略集合中达到最优效果的策略称为最优策略。</li><li data-pid="wNU9bB7E"><b>最优化原理</b>。如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。最优化原理实际上是要求问题的最优策略的子策略也是最优的。</li></ul><p data-pid="mjlOIDJP"><br/>动态规划算法是从底向上进行的,然后由底层开始对每层的关系做两两连接(如果满足的话),构造出上层,逐次递推到树根。下面介绍具体步骤。</p><p data-pid="87OS7IsA"><b>步骤1 初始状态</b></p><p data-pid="1gqk7aY-">构造第一层关系,每个叶子对应一个单表,为每一个待连接的关系计算最优路径(就是单表的最佳访问方式,通过评估不同的单表的数据扫描方式花费)。</p><p data-pid="3l5Cg_nF"><b>步骤2 归纳</b></p><p data-pid="Y4D8BBHl">当层数从第1到n-1,假设已经生成,则求解第n层关系的方法为:</p><ul><li data-pid="RPqiaarE">将第n-1层的(多个)关系与第一层中的每个关系连接,生成新的关系且进行估算</li><li data-pid="mIZj_6Dn">将每一个新关系放于第n层,且均求解其最优路径。</li></ul><p data-pid="gcMSAXgD">步骤2会被多次执行,每层路径的生成都是基于上层生成的最优路径的,这满足最优化原理的要求。<br/>PostgreSQL查询优化器求解多表连接时,采用了这种算法。</p><p data-pid="JKS6kg_y">启发式算法(heuristic algorithm)是相对于最优化算法提出的,是一个基于直观或经验构造的算法。在逻辑查询优化阶段和物理查询优化阶段,都有一些启发规则可用。<br/>启发式方法不能保证找到最好的查询计划。PostgreSQL、MySQL和Oracle等数据库在实现查询优化器时,采用了启发式和其他方式相结合的方式。<br/><br/>启发式是根据已知可优化规则,对SQL语句做出语义等价转换的优化,或者基于经验对某个物理操作进行改进(如物化操作)。<br/><br/>在物理查询优化阶段常用的启发式规则如下:</p><ul><li data-pid="SthTAARQ">关系R在列X上建立索引,且对R的选择操作发生在列X上,则采用索引扫描方式。</li><li data-pid="dQiNOkuH">R连接S,其中一个关系上的连接列存在索引,则采用索引连接且此关系作为内表。</li><li data-pid="Df-kHHWY">R连接S,其中一个关系上的连接列是排序的,则采用排序进行连接比Hash连接好。</li></ul><p data-pid="RkxxA8rY">又称贪心算法。在对问题求解时,贪婪算法总是选择当前看来是最好的,是局部最优,不一定是整体最优。不从整体最优上考虑,省去了为找最优解要穷尽所有可能而必须耗费的大量时间(动态规划算法就是这么做的),得到的是局部最优解。<br/><br/>贪婪算法为了解决问题需要寻找一个构成解的候选对象集合。其主要实现步骤如下:<br/>1)初始,算法选出的候选对象的集合为空;<br/>2)根据选择函数,从剩余候选对象中选出最有希望构成解的对象;<br/>3)如果集合中加上该对象后不可行,那么该对象就被丢弃并不再考虑;<br/>4)如果集合中加上该对象后可行,就加到集合里;<br/>5)扩充集合,检查该集合是否构成解;<br/>6)如果贪婪算法正确工作,那么找到的第一个解通常是最优的,可以终止算法;<br/>7)继续执行2,(每做一次贪婪选择就将所求问题简化为一个规模更小的子问题,最终可得到问题的一个可能的整体最优解)。<br/><br/>MySQL查询优化器求解多表连接时采用了这种算法。</p><p data-pid="PqR6JCXi">System R算法对自底向上的动态规划算法进行了改进,主要的思想是把子树的查询计划的最优查询计划和次优的查询计划保留,用于上层的查询计划生成,以便使得查询计划总体上最优。</p><p data-pid="04rHyJDm">遗传算法(Genetic Algorithm,GA)是一种启发式的优化算法,是基于自然群体遗传演化机制的高效探索算法。其模拟自然界生物进化过程,采用人工进化的方式对目标空间进行随机化搜索。根据预定的目标适应度函数对每个个体进行评价,不断得到更优的群体,同时以全局并行搜索方式来搜索优化群体中的最优个体,求得满足要求的最优解。<br/><br/>遗传算法中的主要概念:</p><ul><li data-pid="EbVwf9s5"><b>群体</b>(population)。一定数量的个体组成了群体,表示GA的遗传搜索空间。</li><li data-pid="LpqJFvMz">个体(individual)。多个个体组成群体,在多表连接中是每个基本关系或中间生成的临时关系。</li><li data-pid="jQ6xM2Jx"><b>染色体</b>(chromosome)。个体的特征代表,即个体的标志,由若干基因组成,是GA操作的基本对象,所以操作个体实则是操作染色体(个体几乎可以简单理解为“等同染色体”)。染色体用字符串表示。</li><li data-pid="PZeJj1B-"><b>基因</b>(gene)。基因是染色体的片段,多段基因组成染色体,基因变异导致基因不断被优化。</li><li data-pid="VrCwORmr"><b>适应度</b>(fitness)。表示个体对环境的适应程度,通常由某一适应度函数表示。对应执行策略的执行代价。</li><li data-pid="HUmjOlKs"><b>选择</b>(selection)。根据个体的适应度,在群体中按照一定的概率选择可以作为父本的个体,选择依据是适应度大的个体被选中的概率高。</li><li data-pid="k_5aUK4B"><b>交叉</b>(crossover)。将父本个体按照一定的概率随机地交换基因形成新的个体。</li><li data-pid="GUWuxLtX"><b>变异</b>(mutate)。按一定概率随机改变某个个体的基因值。</li></ul><p data-pid="LpvgCZMx">遗传算法涉及的关键问题。</p><ul><li data-pid="uMWxzE9a"><b>串的编码方式</b>。本质是编码问题。一般把问题的各种参数用二进制形式进行编码,构成子串;然后把子串拼接构成“染色体”串。串长度及编码形式对算法收敛影响极大。</li><li data-pid="5dH0qC3L"><b>适应度函数的确定</b>。适应度函数(fitnessfunction)又称对象函数(object function)或问题的“环境”,是问题求解品质的测量函数。一般可以把问题的模型函数作为对象函数,但有时需要另行构造。</li><li data-pid="3cxAOzG7"><b>遗传算法自身参数设定</b>。遗传算法3个自身参数:群体大小n、交叉概率Pc和变异概率Pm,具体如下:</li><ul><li data-pid="1JJS0eR1">群体大小n,太小时难以求出最优解,太大则增长收敛时间,一般n=30~160。</li><li data-pid="8dpJIBZY">交叉概率Pc,太小时难以向前搜索,太大则容易破坏高适应值的结构,一般取Pc=0.25~0.75。</li><li data-pid="iCqDrTSI">变异概率Pm,太小时难以产生新的基因结构,太大使遗传算法成了单纯的随机搜索,一般取Pm=0.01~0.2。</li></ul></ul><p data-pid="cQwa27CN">遗传算法主要步骤如下:</p><p data-pid="2G40uFZo">1)随机初始化种群;<br/>2)评估初始的种群,即为种群计算每个个体的适应值且对所有个体排序;<br/>3)如果没有达到预定演化数,则继续下一步,否则结束算法;<br/>4)选择父体,随机挑选父体dad和母体mum;<br/>5)杂交,父体和母体杂交得到新个体child;<br/>6)变异,在某些个别条件下对新个体变异;<br/>7)计算新个体的适应值,并把适应值排名插入到种群,种群中排名最后的则被淘汰;<br/>8)继续步骤3)。</p><p data-pid="kfGnAO5J">还有其他的一些算法,都可以用于查询优化多表连接的生成,如爬山法、分支界定枚举法、随机算法、模拟退火算法或多种算法相结合等。</p><p data-pid="em6QwEHF">多表连接常用算法比较表(表3-5):</p><figure data-size="normal"><img src="https://pic2.zhimg.com/v2-bea37afbef41078025604093c45b0081_r.jpg" data-caption="" data-size="normal" data-rawwidth="1500" data-rawheight="507" class="origin_image zh-lightbox-thumb" width="1500" data-original="https://pic2.zhimg.com/v2-bea37afbef41078025604093c45b0081_r.jpg"/></figure><ul><li data-pid="yy5bHsa5">《数据库查询优化的艺术:原理解析和SQL性能优化》,李海翔</li></ul><p></p>
<blockquote data-pid="749CJQ5a">作者信息:<br/>范振(花名辰繁),阿里云计算平台-开源大数据-OLAP方向负责人,高级技术专家,StarRocks Community Champion。</blockquote><p data-pid="BYwoEtQs"><br/>随着阿里云EMR StarRocks 上线,在和用户交流的过程中,越来越多被问到 StarRocks 和 ClickHouse 的区别,其中 Join 能力最受客户关心。提到 Join,最为重要的便是 Optimizer 的实现,所以我来写一篇关于 Optimizer 的详解文章,希望给大家一个全面的理解。<br/>StarRocks 作为近年来非常优秀的 OLAP 引擎,在 Planner/Optimizer 上有高效、稳定的实现,这篇文章会从分析主流 Optimizer 框架的模型入手,详细解构 StarRocks 的 Optimizer 实现过程。<br/>内容提要:<br/></p><ul><li data-pid="PO_AHZed">Cascades/Orca 论文涉及的 Top-Down 优化思路与分析。</li><li data-pid="DoFmbUQj">针对 CMU15-721 的一些 PPT、观点、结论加以解析。</li><li data-pid="CkRYkS62">着重结合 StarRocks 的实现,并介绍 StarRocks 的 Optimizer 主要借鉴的 CMU noisepage(<a href="https://link.zhihu.com/?target=https%3A//github.com/cmu-db/noisepage" class=" external" target="_blank" rel="nofollow noreferrer"><span class="invisible">https://</span><span class="visible">github.com/cmu-db/noise</span><span class="invisible">page</span><span class="ellipsis"></span></a>)项目以及 Cascades/Orca 论文的思路。</li></ul><p data-pid="-D39Fcdc">SQL 优化流程图<br/>下图体现了SQL文本到最终的分布式Physical Plan的全流程:<br/></p><figure data-size="normal"><img src="https://pic3.zhimg.com/v2-850d9491e1997d3ba2891307dd84a796_r.jpg" data-caption="" data-size="normal" data-rawwidth="920" data-rawheight="370" class="origin_image zh-lightbox-thumb" width="920" data-original="https://pic3.zhimg.com/v2-850d9491e1997d3ba2891307dd84a796_r.jpg"/></figure><p class="ztext-empty-paragraph"><br/></p><ul><li data-pid="53ag_JYg">Analyzer 需要结合内部/外部 Catalog 系统,主要是检查 Table、Column 等信息是否合法。</li><li data-pid="lpElDN3O">Rewriter 阶段(RBO)主要是一些 Logical->Logical 的变换操作,基于一些经典的代数变换来进行。</li><li data-pid="vnTuHtZP">Optimizer(CBO)需要结合内部/外部的 Cost 模块,常用的计算采集信息包括行数、列基数、列最大最小值、每行平均大小、直方图等信息。</li></ul><p data-pid="tBnCm8sW"> 本文主要聚焦在 CBO 阶段的技术原理解析。<br/></p><p data-pid="Y12Fbfgj">Tree Rewrite 的过程大体思路是:<br/></p><ul><li data-pid="g6FUzJzl">本质上是二叉树的转换,利用已知的 Transformation Rule 和已知的 Pattern,对逻辑二叉树做匹配、转换,形成一棵新的二叉树。</li><li data-pid="0JUkwrWq">Top-Down 的迭代(即每条 Rule 自顶向下的匹配 Tree 以及 Subtree)方式,将所有符合规则的 Rule全部应用到逻辑二叉树,形成新的逻辑二叉树。</li></ul><p data-pid="8YZHT1TA">我们以下图的 Predicate PushDown 为例进行分析:<br/></p><figure data-size="normal"><img src="https://pic1.zhimg.com/v2-866c00ea58bb1351187dd50dd6b26d40_r.jpg" data-caption="" data-size="normal" data-rawwidth="920" data-rawheight="494" class="origin_image zh-lightbox-thumb" width="920" data-original="https://pic1.zhimg.com/v2-866c00ea58bb1351187dd50dd6b26d40_r.jpg"/></figure><p data-pid="rEG0rbKE"><br/>每一个 Transformation Rule 有一个 Pattern,首先会检查 Rule 的 Pattern 是否能够 Match Logical Tree,本例中以下的 Rule 可以匹配上图的 Tree。<br/></p><figure data-size="normal"><img src="https://pic1.zhimg.com/v2-e54e29fd515a40961bcce9b25d51106c_r.jpg" data-caption="" data-size="normal" data-rawwidth="860" data-rawheight="148" class="origin_image zh-lightbox-thumb" width="860" data-original="https://pic1.zhimg.com/v2-e54e29fd515a40961bcce9b25d51106c_r.jpg"/></figure><p class="ztext-empty-paragraph"><br/></p><ul><li data-pid="-pmidWhb">确认能够匹配之后,就是 Tree 的 Transform 过程,针对本例大体流程是:<br/></li><ul><li data-pid="oxekDaAp">将 FilterOperator 的 Predicates 分裂开,可以得到几个 Predicates。</li><li data-pid="nkPw3P_k">根据 Pattern 的数据结构可知,取 FilterOperator 的子节点,一定是 JoinOperator 。将匹配的 Predicates 分给 FilterOperator,将不匹配的 Predicates 生成新的 FilterOperator 挂到FilterOperator 的 2 个子节点之上、JoinOperator 之下。</li><li data-pid="2CMRwnp7">这样就完成了一层下推动作,同理继续迭代的进行 Subtree 的匹配、下推操作。</li></ul></ul><p class="ztext-empty-paragraph"><br/></p><p data-pid="T_UnaIFy">当所有的 Rules 都进行了一次匹配操作之后(如果 Match 不匹配,不进行 Transform),Rewriter 的工作就结束了,得到了一个新的 Logical Plan。<br/></p><p data-pid="VZr60G0Q"><br/>CBO 的目标是将一棵重写后的 Logical Tree 转换为 Physical Tree,使得这棵 Physical Tree 的执行代价最小,或者是在一定约束下的“最小”(这里的条件实质指的是 Property,每个 Property 对应一个最小代价的 Tree,或者说 Plan)。<br/>查询优化的整体思路是:<br/></p><ul><li data-pid="TsnvcVRp">通过逻辑变换(代数变换),找到所有等价的 Logical Plan,例如 Join 的交换律、结合律等。</li><li data-pid="N2XTuEkW">将 Logical Plan 转变成 Physical Plan,例如将 LogicalJoin 拓展为 PhysicalHashJoin、PhysicalSortMergeJoin、PhysicalNestLoopJoin 等,将 LogicalScan 拓展为 SeqScan、IndexScan等。</li><li data-pid="6nK908Ko">第一步、第二步确立了完整的、统一的搜索空间。</li><li data-pid="SPC2Wa5Y">根据不同算子的代价模型,计算出每一种 Physical Plan 的代价。</li><li data-pid="ta_41f9I">选取代价最小的作为最终的 Physical Plan。</li></ul><p data-pid="vQMgt8fc">但是,面临着几个问题:<br/></p><ul><li data-pid="OXAz3gfY">根据逻辑计划推导出的物理计划特别多,多表 Join 膨胀的数量级比较大,在有限时间内可能无法全部计算。</li><li data-pid="vXlCZVSa">要知道哪些 Subtree 的代价被计算过了,要知道哪些 Transformation Rule 针对哪些算子被应用过了。</li><li data-pid="-Cf_nE9a">需要尽量通过减少搜索空间,尽早地剪枝。</li><li data-pid="dhemoBCD">如何评估每个算子的代价。</li></ul><p data-pid="qSi2Xu5u">查询优化的几个比较重要的原则,参考《CMU15-721-Optimizer》中所述:<br/></p><ul><li data-pid="SuSxGTyr">对于一个给定的查询,找到一个正确的,最低“cost”的执行计划</li><li data-pid="KU96fxTM">这是数据库系统中最难实现好的一部分(是一个 NP 完全问题)</li><li data-pid="M-4VpOnI">没有优化器能够真正产生一个“最优的”计划,我们总是<br/></li><ul><li data-pid="oH7f5R8p">用估算的方式去“猜”真实计划的 cost</li><li data-pid="MBP42vBZ">用启发式(heuristics)的方式去限制搜索空间的大小</li></ul></ul><p class="ztext-empty-paragraph"><br/></p><p data-pid="RZsR8ez6">早期关于 CBO 、特别是 Bottom-Up 系列的优化器,应用很少,忽略不讲。关于现代优化器的一些Paper 的研究情况大致如下:<br/></p><ul><li data-pid="GHMK-v-2">Volcano 是更早的 Top-Down 优化框架,现代数据库已经没有落地,忽略不讲。</li><li data-pid="ywOrMv6c">Cascades 是改进 Volcano 的另一种基于 Top-Down 的优化框架,是现代数据库应用的最多的 Optimizer 框架,论文比较抽象,指出了很多方法论,实践意义比较强。</li><li data-pid="SEbQTan_">ORCA 是针对大数据场景的、基于 DXL 通信的 Standalone 的优化器(可以把优化器部分抽出来,独立 service 部署),它引入了 Distribution(包括 Shuffle、Broadcast 等网络传输算子)的Property,规范了可能的数据分布。ORCA 可以认为是 Cascades 的优化实现版本,给出了更加丰富的实现细节和步骤。</li></ul><p data-pid="SsMUziqd"><br/>基础概念 <br/></p><ul><li data-pid="SEeUsgKj">Expressions </li></ul><figure data-size="normal"><img src="https://pic4.zhimg.com/v2-b8c1fca4b3efd3202c595c746a52ce87_r.jpg" data-caption="" data-size="normal" data-rawwidth="686" data-rawheight="418" class="origin_image zh-lightbox-thumb" width="686" data-original="https://pic4.zhimg.com/v2-b8c1fca4b3efd3202c595c746a52ce87_r.jpg"/></figure><p class="ztext-empty-paragraph"><br/></p><ul><li data-pid="E_fhWB6Q">Groups</li></ul><figure data-size="normal"><img src="https://pic1.zhimg.com/v2-722546ddc19fd8342613bf225b927790_r.jpg" data-caption="" data-size="normal" data-rawwidth="686" data-rawheight="398" class="origin_image zh-lightbox-thumb" width="686" data-original="https://pic1.zhimg.com/v2-722546ddc19fd8342613bf225b927790_r.jpg"/></figure><p class="ztext-empty-paragraph"><br/></p><ul><li data-pid="lbfflF49">Rules</li></ul><figure data-size="normal"><img src="https://pic4.zhimg.com/v2-05597ca44439c5fb595ca1e19280e723_b.jpg" data-caption="" data-size="normal" data-rawwidth="412" data-rawheight="250" class="content_image" width="412"/></figure><p class="ztext-empty-paragraph"><br/></p><figure data-size="normal"><img src="https://pic2.zhimg.com/v2-e2b024225c13aabe7fd2680113f9192d_r.jpg" data-caption="" data-size="normal" data-rawwidth="480" data-rawheight="244" class="origin_image zh-lightbox-thumb" width="480" data-original="https://pic2.zhimg.com/v2-e2b024225c13aabe7fd2680113f9192d_r.jpg"/></figure><p class="ztext-empty-paragraph"><br/></p><ul><li data-pid="hAB_im39">Memo</li></ul><figure data-size="normal"><img src="https://pic4.zhimg.com/v2-76f42feaeab8d98cae69f41a4b46402b_r.jpg" data-caption="" data-size="normal" data-rawwidth="672" data-rawheight="390" class="origin_image zh-lightbox-thumb" width="672" data-original="https://pic4.zhimg.com/v2-76f42feaeab8d98cae69f41a4b46402b_r.jpg"/></figure><p data-pid="7ocw9faT"><br/>核心优化流程<br/></p><ul><li data-pid="JKDHREoT">Exploration,将原始的 Logical Plan 转成等价的 Logical Plan,比如 Join 的结合律、交换律等。</li><li data-pid="xAYYplHC">Statistics Derivation,将所有的逻辑计划,沿着 Tree->Subtree Top-Down 方式进行统计信息搜集。实现中,可以认为是 Tree 的后根遍历,先进行子节点的 Statistics 搜集,再根据子节点提供的Statistics 搜集自身的 Statistics。</li></ul><figure data-size="normal"><img src="https://pic3.zhimg.com/v2-63e5caccfb2b16898390d4b3362e09f6_r.jpg" data-caption="" data-size="normal" data-rawwidth="510" data-rawheight="424" class="origin_image zh-lightbox-thumb" width="510" data-original="https://pic3.zhimg.com/v2-63e5caccfb2b16898390d4b3362e09f6_r.jpg"/></figure><p class="ztext-empty-paragraph"><br/></p><ul><li data-pid="JpcfffGM">Implementation,将所有 Exploration 得到的 Logical Plan 转成对应的 Physical Plan,转换过程中一个 Logical Plan 对应着多个 PhysicalPlan,比如 LogicalJoin,可以转换为 PhysicalHashJoin、PhysicalSortMergeJoin、PhysicalNLJoin等。</li><li data-pid="bqz8xG1A">Optimization,开始进行真正的计算代价,这部分会在后面的“优化的流程”章节中详细说明。</li></ul><p data-pid="fiEY31C5">需要注意的是:真正实现过程中,不应该顺序按照这四步来,至少 StarRocks 不是这么做的。CMU15-721 以及 Cascades 论文都有解释:<br/></p><ul><li data-pid="8GAyyjVQ">Stratified search vs unified search,分层搜索是先 Logical->Logical,再 Logical->Physical;统一搜索是 Logical->Logical 以及 Logical->Physical 一起都做了,都属于同一个 Group 中的等价变换。</li><li data-pid="iTzG7iaZ">这样做的好处是,在 Exploration Logical Plan 时,有可能其 Subtree 对应的 Physical plan 已经不满足条件(比如由于 Cost 过大,被剪枝了),所以不需要继续进行。Cascades 中与此思想接近的描述是:Cascades 搜索引擎确保只有那些真正可以参与查询评估计划的子树和相关联的(interesting)属性得到优化。每次当一个输入被优化之后,优化任务可以获得一个最小的 cost,用这个 cost 去限制下一次的优化输入上限。这样,可以尽可能地紧凑(tight)剪枝。</li><li data-pid="rzQp_Wyd">对于一个 Tree 或者 Subtree,它的 Cost 是逐渐递减的。会将上一次 Tree 计算得到的 Cost 结果当做下一次 Tree 的 Limit,这样有利于搜索剪枝,即一旦 Subtree 计算 Cost 的过程中超过 Limit,会立即被剪枝。</li><li data-pid="CtV-6wZb">这样做需要更多的 Memo 存储空间。</li></ul><p data-pid="_B83M6AF">Property Enforcement<br/></p><p data-pid="WeNeBgI7">在 ORCA 框架中 Property 有多种,这里重点介绍 Sort 和 Distribution,我们用{Sort,Distribution}表示,如果均为任意属性,表示为{Any, Any}。Property 实际上就是最终某个算子需要什么样的特性(Required Properties),举以下例子:<br/></p><ul><ul><li data-pid="Uj4pyXPj">Select a from A order by a。这个 Query 希望最终能够能够按照 Column a 来排序,那么我们会在查询最后加入对于 a 的排序特性来满足最终的需求,那么对于这个 Query 要求{Sort(A.a), Any}。同时父节点需要把 Required Property 传导下去,给到 Scan A 子节点。这样我们面临着两种选择:<br/></li><ul><li data-pid="xTvDCewg">要求子节点 Scan A 排序,即{Sort(A.a), Any},父节点 Project 不需要排序直接输出。</li><li data-pid="i803TBdh">要求子节点不排序,即{Any, Any},父节点加入排序算子(即需要Enforce操作)后直接输出。</li><li data-pid="ngDgLIDz">以上两种策略供我们选择,我们可以潜在利用 A 表的有序性,进行 IndexScan(例如利用 B+Tree 的有序性)。</li></ul></ul></ul><p class="ztext-empty-paragraph"><br/></p><ul><ul><li data-pid="yovU0vlc">Select * from A Join B where A.a=B.b。这个 Query 中,我们对于Join结果没有分布要求, Property 可以为 Any。针对 PhysicalHashJoin 算子,我们有两组 Required Properties<br/></li><ul><li data-pid="BzF58UrW">左表A 是{Any, Any},右表B是{Any, Broadcast}。</li><li data-pid="Jzcr9V0U">左表A是{Any, Hash<A.a>},右表B是{Any, Hash<B.b>}。</li></ul></ul></ul><p class="ztext-empty-paragraph"><br/></p><p class="ztext-empty-paragraph"><br/></p><p data-pid="CrjOIkGh"><b>为了满足Required Properties,我们需要针对物理算子的实际行为,进行</b> <b>Enforcer。比如</b> <b>A</b> <b>表并没有按照</b> <b>Hash<A.a></b> <b>进行分布,那么就需要加入</b> <b>Shuffle[A.a]</b> <b>算子。如果需要A表Broadcast,需要加入Broadcast算子。</b><br/>加入了对应的 Enforcers,由于引入了新的算子(例如 Shuffle、Broadcast),我们计算 Cost,需要评估 Enforcers 本身的 Cost。<br/>优化的流程<br/></p><p data-pid="oQBRaZL7">在分布式系统中,每一个 Group 对应的 Best Plan 实际上都是针对某一 Property 来说的,在 StarRocks 中是如下数据结构<br/></p><ul><ul><li data-pid="EpRH2t78">Map<PhysicalPropertySet, Pair<Double, GroupExpression>> LowestCostExpressions。代表每一个 Group 中,满足 Required Property 条件的最佳 Expression(GroupExpression 可以简单理解为与 Expression 等价)。</li><li data-pid="bYvKASQ6">Map<PhysicalPropertySet, Pair<Double, List<PhysicalPropertySet>>> LowestCostTable。代表每一个 GroupExpression 中,满足了 Required Property 条件的节点,它的子节点需要满足的Required Properties。</li></ul></ul><p class="ztext-empty-paragraph"><br/></p><p data-pid="VzxlyXwt">所以 Optimizer 的工作就是不断根据 Rules Transformation 拓展得到 Logical/Physical Plan,插入到 Memo 中,通过比较 Cost 来不断迭代更新这两个数据结构。在搜索的过程中通过 Cost 来提早的Prune无效的Subtree。<br/>下图为 ORCA 最终的静态图,起始的 Required Property 为{Singleton, <T1.a>},通过以上两个数据结构,最终得到了 Best Plan。<br/></p><figure data-size="normal"><img src="https://pic2.zhimg.com/v2-33aa2fba06ccfcdce5c8c3f234bb9de1_r.jpg" data-caption="" data-size="normal" data-rawwidth="920" data-rawheight="402" class="origin_image zh-lightbox-thumb" width="920" data-original="https://pic2.zhimg.com/v2-33aa2fba06ccfcdce5c8c3f234bb9de1_r.jpg"/></figure><p data-pid="xWK9n7uJ"><br/>动态规划算法解释<br/></p><ul><li data-pid="ZRUCNIbD">整体的搜索过程是 DP 算法,最小 Cost 是问题(即DP[property_a,n])的最优解,它是所有 Exploration 得到的 Physical Plan 的解空间(即DP[property_b, n-1],DP[property_c,n-1]...)中,满足特定 Property 的最小 Cost 值。这样问题就可以变成求子树的最小 Cost,就可以归为 DP 问题。</li><li data-pid="TiJabezN">利用 Memo 来做 DP Memorization,每一个 Group 记录某一 Required Property 对应的 Best Expr。</li><li data-pid="TBXyYITS">通过 Cost Limit 的不断递减来做剪枝操作。</li></ul><ul><li data-pid="LRzez7xZ">在 StarRocks 每一个 Logical Plan 中,Physical Plan 都可以认为是 OptExpression,下文我们称为 Logical Expression 和 Physical Expression。其中的 GroupExpression 和 OptExpression 可以互相经过变换生成。</li><li data-pid="jR4r2Qgc">起初的 Root OptExpression(经过 rewriter 后的 Logical Plan)经过封装形成 Root GroupExpression 后,后续基本所有的操作都是基于 GroupExpression 数据结构的。</li></ul><figure data-size="normal"><img src="https://pic3.zhimg.com/v2-4499e6c5b34e4b9b01722021b0cc8ec2_r.jpg" data-caption="" data-size="normal" data-rawwidth="454" data-rawheight="298" class="origin_image zh-lightbox-thumb" width="454" data-original="https://pic3.zhimg.com/v2-4499e6c5b34e4b9b01722021b0cc8ec2_r.jpg"/></figure><p class="ztext-empty-paragraph"><br/></p><figure data-size="normal"><img src="https://pic4.zhimg.com/v2-2b35df81ab6db99c95d165b2959eb407_b.jpg" data-caption="" data-size="normal" data-rawwidth="406" data-rawheight="300" class="content_image" width="406"/></figure><p class="ztext-empty-paragraph"><br/></p><ul><li data-pid="-ca0rEO5">StarRocks 基本遵循了 Cascades/ORCA 的思想,进行了一部分的改进,这些优化在上文中也有过描述,不再赘述,整体的架构如下。</li></ul><figure data-size="normal"><img src="https://pic2.zhimg.com/v2-9fdaf9deb00393799dc97819072a3529_r.jpg" data-caption="" data-size="normal" data-rawwidth="782" data-rawheight="346" class="origin_image zh-lightbox-thumb" width="782" data-original="https://pic2.zhimg.com/v2-9fdaf9deb00393799dc97819072a3529_r.jpg"/></figure><p class="ztext-empty-paragraph"><br/></p><ul><li data-pid="Jeu19MQ-">StarRocks 中的 TaskScheduler 基于 Stack 和 OptimizerTask 抽象,实现了一套任务调度框架,Task 的类型有以下几种:</li></ul><figure data-size="normal"><img src="https://pic1.zhimg.com/v2-716ec33cf8dc31bb52ddbbf35d0e4c80_r.jpg" data-caption="" data-size="normal" data-rawwidth="724" data-rawheight="264" class="origin_image zh-lightbox-thumb" width="724" data-original="https://pic1.zhimg.com/v2-716ec33cf8dc31bb52ddbbf35d0e4c80_r.jpg"/></figure><p class="ztext-empty-paragraph"><br/></p><ul><li data-pid="z4C3CF4r">DeriveStatsTask 对应 ORCA 中的 Statistics Derivation 过程,通过树的后根遍历来获得所有节点的 Statistics,注意这里的 Statistics 不是 Cost,通过 Logical Plan(主要是根据表、列的元信息统计)就可以获得每个 Group 对应的 Statistics。</li><li data-pid="6nn0bKPX">OptimizeExpressionTask 对应 ORCA 中的 Exploration 和 Implementation 过程。针对所有的 Rules(当前有 26 个 Implementation Rule 和 6+ 个 Transformation Rule)进行 Pattern 匹配,能够匹配 GroupExpression 对象的成为 Valid Rule,形成 ApplyRuleTask 对象。</li><li data-pid="48vgOjn6">ApplyRuleTask 对应 ORCA 中的 Exploration 和 Implementation 过程。将 Rule 应用到 Logical Plan 中,实现 Logical->Logical、Logical->Physical 的转换,通过等价变换拓展每个 Group 的搜索空间。</li><li data-pid="DgucwhEM">EnforceAndCostTask 对应了计算 Physical Plan 的 Cost 的过程,如果某个 Expression 不满足Property,会 Enforce 出其他 Operator,例如 Broadcast、Shuffle、Sort 等算子。</li></ul><p data-pid="si-XKR_r"><br/>下面几张图以三表 Join 为例,体现了 StarRocks 的 Memo 不断变化的过程,最终通过上文中提到的 2 个数据结构的不断更新,得到了最终的 Best Plan。<br/></p><ul><li data-pid="SBcv-kkd">Memo Init 的过程,直接把三表 Join Rewriter 之后的 Logical Plan 插入到 Memo 中,得到了 6 个 Group。</li></ul><figure data-size="normal"><img src="https://pic2.zhimg.com/v2-7df42c2085c50476f1c9e048aceaaea5_r.jpg" data-caption="" data-size="normal" data-rawwidth="654" data-rawheight="356" class="origin_image zh-lightbox-thumb" width="654" data-original="https://pic2.zhimg.com/v2-7df42c2085c50476f1c9e048aceaaea5_r.jpg"/></figure><p class="ztext-empty-paragraph"><br/></p><ul><li data-pid="Ewc_mD6A">通过 Implementation Rule 对每个 Group 通过 Top-Down遍历(利用 Stack 完成树的后根遍历)进行 Logical->Physical、Logical->Logical 的拓展。下图为 Join 交换律,由于 Join 交换律并没有增加新的 Join 组合,所以并没有新增 Group。</li></ul><figure data-size="normal"><img src="https://pic3.zhimg.com/v2-52e87974f63568274a9e0ddbd6e2c66a_r.jpg" data-caption="" data-size="normal" data-rawwidth="652" data-rawheight="292" class="origin_image zh-lightbox-thumb" width="652" data-original="https://pic3.zhimg.com/v2-52e87974f63568274a9e0ddbd6e2c66a_r.jpg"/></figure><p class="ztext-empty-paragraph"><br/></p><ul><li data-pid="NYHzOVqy">同样地,通过 Top-Down 遍历利用 Join 结合律进行 Transformation,由于 Join 结合律生成了新的 Join 组合,所以会新增 Group。</li></ul><figure data-size="normal"><img src="https://pic4.zhimg.com/v2-19b7506488a4ab3f4b549f245f3635ab_r.jpg" data-caption="" data-size="normal" data-rawwidth="652" data-rawheight="304" class="origin_image zh-lightbox-thumb" width="652" data-original="https://pic4.zhimg.com/v2-19b7506488a4ab3f4b549f245f3635ab_r.jpg"/></figure><p class="ztext-empty-paragraph"><br/></p><ul><li data-pid="8cD4V_P4">不断进行 Cost 计算,通过设置 Cost Limit 收敛进行剪枝,通过 Memo/Group 来记录每种不同的 Required Property 对应的 Best Expr,来更新上文提到的 2 个数据结构,最终到 Best Plan。</li></ul><figure data-size="normal"><img src="https://pic2.zhimg.com/v2-5d3898d1266fca7c7f5095f8e247937d_r.jpg" data-caption="" data-size="normal" data-rawwidth="654" data-rawheight="324" class="origin_image zh-lightbox-thumb" width="654" data-original="https://pic2.zhimg.com/v2-5d3898d1266fca7c7f5095f8e247937d_r.jpg"/></figure><p data-pid="GJ2MfBzW"><br/>StarRocks 中每个 Group 的 Statistics 有以下变量<br/></p><ul><li data-pid="6d2urAW-">double outputRowCount</li><li data-pid="izBL0P99">Map<ColumnRefOperator, ColumnStatistic> columnStatistics</li></ul><p data-pid="pwDcL-KN">其中每个 Column 对应一个 ColumnStatistic 对象,主要有以下变量<br/></p><ul><ul><li data-pid="vNc4TVR_">minValue</li><li data-pid="fka7CqXq">maxValue</li><li data-pid="g0EoGHGg">averageRowSize</li><li data-pid="1fyaVNA_">distinctValuesCount</li><li data-pid="Dhpvt4Mg">...</li></ul></ul><p class="ztext-empty-paragraph"><br/></p><p data-pid="0I-p38sP">有了这些 Stats,我们其实可以预估任意一个算子的 Cost,比如预估 HashJoin 的方法如下图,即:<br/></p><ul><ul><li data-pid="9JpgppHu">cpuCost 为左右孩子输出的数据大小之和,即行数*每行对应的 avgRowSize。注意:这个avgRowSize 统计的是经过 Column Prune 之后的列数对应的平均行大小。</li><li data-pid="biOQXiBv">memoryCost 为右孩子的输出数据大小。做 HashJoin,右表为 Builder 表,占用内存,如果是 crossJoin 会计算对应的 Penalty(此处为10^8L)。</li><li data-pid="kaYLK1sY">networkCost 此处为 0,因为 HashJoin 本身不发生任何的网络交换。</li></ul></ul><p class="ztext-empty-paragraph"><br/></p><figure data-size="normal"><img src="https://pic3.zhimg.com/v2-ac7e092bbe54c56f8ef7093d5794ffaa_r.jpg" data-caption="" data-size="normal" data-rawwidth="920" data-rawheight="228" class="origin_image zh-lightbox-thumb" width="920" data-original="https://pic3.zhimg.com/v2-ac7e092bbe54c56f8ef7093d5794ffaa_r.jpg"/></figure><p class="ztext-empty-paragraph"><br/></p><p data-pid="-HI-Xjp-">参与采集、计算 Stats 相关的类为:<br/></p><ul><ul><li data-pid="bFpfI6Zo">CreateAnalyzeJobStmt,负责建立采集信息的异步 Schedule Job。</li><li data-pid="pYPk2SBs">AnalyzeStmt,手动 Analyze 命令搜集 Stats。</li><li data-pid="dTnTsoyy">StatisticAutoCollector,负责 Schedule 采集 Stats。</li><li data-pid="Vfy4_5vn">StatisticsCalulator,真正的核心类,负责计算各种算子的 Statistics,在上文提到的 DeriveStatsTask 中调用。</li></ul></ul><p data-pid="gTJD7HfS">【1】<a href="https://link.zhihu.com/?target=https%3A//www.cse.iitb.ac.in/infolab/Data/Courses/CS632/Papers/Cascades-graefe.pdf" class=" external" target="_blank" rel="nofollow noreferrer"><span class="invisible">https://www.</span><span class="visible">cse.iitb.ac.in/infolab/</span><span class="invisible">Data/Courses/CS632/Papers/Cascades-graefe.pdf</span><span class="ellipsis"></span></a></p><p data-pid="PKaZGIOE">【2】 <a href="https://link.zhihu.com/?target=https%3A//15721.courses.cs.cmu.edu/spring2017/papers/15-optimizer2/p337-soliman.pdf" class=" external" target="_blank" rel="nofollow noreferrer"><span class="invisible">https://</span><span class="visible">15721.courses.cs.cmu.edu</span><span class="invisible">/spring2017/papers/15-optimizer2/p337-soliman.pdf</span><span class="ellipsis"></span></a></p><p data-pid="W975rhcH">【3】<a href="https://link.zhihu.com/?target=https%3A//github.com/StarRocks/starrocks/tree/117716c899fe7649f0c88eedb75fa1621d6ef5f2/fe/fe-core/src/main/java/com/starrocks/sql/optimizer" class=" external" target="_blank" rel="nofollow noreferrer"><span class="invisible">https://</span><span class="visible">github.com/StarRocks/st</span><span class="invisible">arrocks/tree/117716c899fe7649f0c88eedb75fa1621d6ef5f2/fe/fe-core/src/main/java/com/starrocks/sql/optimizer</span><span class="ellipsis"></span></a></p>