赞
踩
开发人员分为三种类型:第一种是知道索引可以加快数据库查询速度,第二种是知道索引可以加快数据库查询速度并占用额外的空间和时间,第三种是对索引及不同的索引类型优点和缺点有更深刻了解的人。一般情况下,第一组和第二组人数最多。
如果您属于前种类型的人员,本文将帮助您理解 SQL 索引。如果您对索引了解很多,本文将帮助您整理知识并提醒您一些好的做法。 SQL 是一种声明性语言,这意味着它告诉数据库我们想要做什么,而不是如何实现它。数据库引擎决定如何提取数据。我们可以通过使用索引来帮助查询规划器。
我在本文中提供的所有信息都是特定于 PostgreSQL 数据库引擎的,但在某些时候,它们对其他流行的数据库引擎也有帮助。我所有的笔记都是基于 iRonin.IT 多年的实践经验——一家顶级软件开发公司,我们为各种技术提供定制软件开发和 IT 人员扩充服务。
想象一下,您运行一个每天都有数百万用户使用的 Web 应用程序。该应用程序使用一个数据库,其中有关用户的信息收集在一个名为 users 的数据库中。每次有人请求查询用户的个人资料时,必须先在数据库中找到用户记录,然后再将信息呈现给访问者。一个普通的场景:
SELECT * FROM users WHERE users.slug = 'user411' LIMIT 1;
上面的查询是针对具有一百万条记录的表执行的,其中每个 slug 都是唯一的,但没有在其上创建任何索引。获得一条记录耗时 57.419 毫秒。让我们在 slug 列上添加一个唯一索引:
CREATE UNIQUE INDEX slug_idx ON users (slug);
再次运行 select 查询,用了 0.055 毫秒就得到匹配的记录。这比以前快了 1043 倍!想象一下,在列上没有适当索引的情况下,对更大的数据集使用更复杂的查询。现在,当我们分析了简单但有意义的示例后,可以深入了解索引的世界,以了解如何以正确的方式设计数据库。
我在开头提到,大多数开发人员都知道索引可以加快对数据库的查询,但是如果你让他们详细解释索引,他们也不知道答案。
我将索引的定义分为两部分:高级定义和低级定义。第一个用来正确理解索引的一般工作方式,第二个是为那些想知道事情在幕后如何工作的人准备的。
想象一下,您的 iPhone 上有一个联系人列表,但这个列表没有按字母顺序排序。你想打电话给John 。你会怎么办?您将从顶部开始浏览每个联系人,然后在名为 John 的联系人处停止。假设您的列表包含 100 个联系人,如果您足够幸运,您需要筛选 20 - 30 个联系人才能找到合适的联系人。如果 John 是最后一个联系人怎么办?找到联系人会花费很多时间。
这就是索引解决的问题。如果没有列上的索引,当进行过滤时,数据库将进行全扫描,这仅仅意味着它会遍历每条记录,除非找到匹配的记录。这可能需要很长时间,具体取决于数据库大小。
当将索引放在列上时,在我们的例子中是联系人的姓名,数据库会创建一个特殊的结构来对联系人进行排序。因此,如果您正在寻找 John,您会寻找以字母 J 开头的联系人姓名;它加快了搜索过程好几倍。
有几种类型的索引;接下来我将讨论它们。我们着重关注默认的 B 树,这样我就可以演示它在数据库内部的低级别上是什么样子的。
B-tree代表平衡搜索树。它通过允许具有两个以上子节点的节点来简化二叉搜索树。每个节点包含按升序排列的键。如果考虑带有联系人姓名的示例,则 B 树结构看起来类似于以下可视化:
如果你将所有这些名字组成一个数组并按字母顺序对它们进行排序,John 将位于数组的中间。如果在索引上执行搜索,执行引擎会检查 给定值 是在根节点之前还是之后,并对每个下一个节点重复检查。
节点还包含指向数据库中记录的指针,因此当找到匹配节点时,引擎将使用该指针从数据库中获取记录并将其合并到搜索结果中。
有多种类型的索引,每一种都适用于不同的使用场景。您已经了解了 B 树索引,它是默认索引,除非您指定另一个索引类型,否则它将被应用。以下是完整列表,以及每种索引类型的使用场景:
事实上,在大多数情况下,您应该可以使用默认创建的 b 树索引,除非您要处理数据库中非常具体的信息。
现在,很清楚索引在一般情况下和底层是如何工作的,就更容易理解我们如何使用索引更新查询以使其更快。
知道为什么以及如何。是时候找到答案了。将索引放在错误的列上不会使您受益;数据库只会占用更多磁盘空间。一般来说,以下做法被认为是好的:
可能还有一些特殊的数据用例,但上述规则适用于大多数应用程序。每次设计数据库、执行遗留数据库审计或寻找提高查询性能的方法时,请考虑它们。
如果不提及帮助我们确定查询规划器如何查看数据的 explain 和 analyze 命令,本节将是不完整的。
EXPLAIN 命令打印查询的执行计划而不执行它。该计划包含操作顺序、连接方法、索引使用情况和估计成本:
EXPLAIN SELECT title FROM articles WHERE published = true;
ANALYZE 命令收集有关表或索引中数据的统计信息。与 EXPLAIN 命令相反,它提供了实际的运行时性能指标:
ANALYZE SELECT title FROM articles WHERE published = true;
您还可以结合使用这两个命令来全面了解 PostgreSQL 中的查询优化和微调:
EXPLAIN ANALYZE SELECT title FROM articles WHERE published = true;
在过滤方面,您可以根据单列或多列过滤记录。如果你打算过滤基本数据类型,默认索引(b-tree)就足够了:
CREATE INDEX title_idx ON articles (title);
如果你打算在多列上添加索引,只需修改命令的最后一部分:
CREATE INDEX title_category_idx ON articles (title, category);
当您创建索引时,默认情况下,您会索引给定表中的全部记录。如果你想避免它,你可以创建一个部分索引。构建查询时,使用 WHERE 关键字来缩小搜索结果范围,创建索引也是如此:
CREATE INDEX title_idx ON articles (title) WHERE published = true;
这样,您将通过仅索引所有记录的子集来减少索引大小。
当简单的值不能满足要求时,您可以根据函数的结果创建索引。一个流行的例子是索引给定字段中值的小写版本:
CREATE INDEX title_idx ON articles (lower(title));
请记住,这样的索引可能会占用更多空间(取决于结果)并且可能会影响数据更新的性能。
您可能已经知道,扫描索引后,再用指针从表中访问数据。您可以跳过第二部分并通过使用覆盖索引进一步改进查询。
覆盖索引也称为仅索引扫描。它包含满足查询所需的所有列,因此无需执行表访问。创建此类索引的语法如下:
CREATE INDEX idx_covering ON articles (title, category) INCLUDE (author, slug);
与往常一样,这样的索引给我们带来了很多好处,但也必须知道,如果**对包含的列进行频繁更新,那么使用这样的索引并不是一个好主意,因为它需要同时更新 索引和原表 **。
除了提到的简单查询之外,您还可以提高更高级的查询的速度。其中包括全文检索和地理空间数据。每当您创建索引时,您都可以参考官方文档,其中详细描述了索引创建命令以及所有可能的参数。
仅仅向数据库添加正确的索引是不够的;您还需要维护现有索引以确保数据库结构处于最佳状态。当你从底层启动应用程序时,你没有太多需要维护的索引,但在遗留应用程序中,有很多情况需要处理。
您需要维护索引,因为过去可能会发生以下情况之一:
这些只是可以创建无效索引的许多情况中的一部分,我们需要维护它们以保持我们的数据库健康和尽可能高性能。
在 PostgreSQL 中,索引的信息存储在 pg_index 表中。由于它是一个表,您可以像查询数据库中的任何其他表一样查询它。我调查了表列(我使用的是 11.19 版),该表包含以下列:
其他列是 indexrelid 、 indrelid 、 indimmediate 、 indcheckxmin 、 indcollation 、 indclass 、 indoption 、 indexprs 、 indpred 。可以查看官方文档中每一列的详细解释。
要检测数据库中未使用的索引,我们可以使用包含使用情况统计信息的 pg_stat_user_indexes 表。该表包含以下重要列:
您必须记住的是,不能简单地找到 idx_scan 等于零的记录。我们还必须记住以下几点:
考虑到以上所有几点,我们最终可以生成一个 SQL 查询,显示哪些索引未被使用:
SELECT s.relname, s.indexrelname, pg_relation_size(s.indexrelid) AS index_size FROM pg_stat_user_indexes s JOIN pg_catalog.pg_index i ON s.indexrelid = i.indexrelid WHERE s.idx_scan = 0 AND 0 <>ALL (i.indkey) AND NOT i.indisunique AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_constraint c WHERE c.conindid = s.indexrelid) ORDER BY pg_relation_size(s.indexrelid) DESC;
上述查询将产生一组结果,其中每个结果包含三个属性:
频繁的更新和删除操作会导致磁盘上的表或索引关系文件有大量未使用的空间;它被称为膨胀。这种情况会导致性能下降。
虽然很多开发人员可能会感到惊讶,但当记录被删除时,它并没有从磁盘上物理删除。 PostgreSQL 使其不可见并标记为删除。更新记录时也会出现同样的情况。在数据库中创建了一个新版本,旧版本被标记为不可见。
在 VACUUM 操作期间,PostgreSQL 扫描表中的死行并将它们从表中删除;只是这种情况会物理删除数据并释放空间。但是,默认情况下不启用自动 VACUUM 操作。
当自动 VACUUM 未启用或启用但运行频率不足以跟上数据库上的工作负载时,就会产生膨胀。您可以通过运行以下查询来检查是否启用了此功能:
SELECT name, setting, unit FROM pg_settings WHERE name = 'autovacuum';
数据库本身提供了一个EXTENSION ,提供有关我们数据库中元组的信息。例如,您可以查询 tables 表,并为每个表触发 pgstattuple 函数并使用以下查询读取死元组的百分比:
SELECT table_name, (pgstattuple(table_name)).dead_tuple_percent AS dead_tuple_percent FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE' ORDER BY dead_tuple_percent DESC;
如果您收到有关未知扩展的错误消息,则必须先启用它:
CREATE EXTENSION pgstattuple;
它现在应该工作了。由于本文是关于索引的,所以让我们看看如何找到索引中的膨胀大小。我们可以使用以下查询:
SELECT cls.relname, am.amname, pg_total_relation_size(indexrelid) AS index_size_bytes, pg_total_relation_size(indexrelid) - pg_relation_size(indexrelid) AS index_bloat_bytes FROM pg_index idx JOIN pg_class cls ON cls.oid=idx.indexrelid JOIN pg_am am ON am.oid=cls.relam WHERE indrelid > 16384;
此查询返回索引的名称、索引类型和估计的膨胀大小。
要从给定索引中删除膨胀,您可以重建索引:
REINDEX INDEX index_name;
上面的命令会将数据从旧索引复制到新索引。请记住,此操作可能需要一些时间并影响数据库性能。
有些情况下存在两个或多个索引;它们具有不同的名称,但列的组合相同。结果,其中一个(甚至更多)未被使用,只是占用空间并使写入过程更加昂贵。
要查找重复项,我们可以使用以下查询查询 pg_index 表:
SELECT pg_size_pretty(sum(pg_relation_size(idx))::bigint) as size, (array_agg(idx))[1] as idx1, (array_agg(idx))[2] as idx2 FROM ( SELECT indexrelid::regclass as idx, (indrelid::text ||E'\n'|| indclass::text ||E'\n'|| indkey::text ||E'\n'|| coalesce(indexprs::text,'')||E'\n' || coalesce(indpred::text,'')) as key FROM pg_index) sub GROUP BY key HAVING count(*) > 1 ORDER BY sum(pg_relation_size(idx)) DESC;
为了更好地理解上面发生的事情,让我们将查询分解成更小的部分。我们有以下子查询:
SELECT indexrelid::regclass as idx, (indrelid::text ||E'\n'|| indclass::text ||E'\n'|| indkey::text ||E'\n'|| coalesce(indexprs::text,'')||E'\n' || coalesce(indpred::text,'')) as key FROM pg_index
我们还有主要查询:
SELECT pg_size_pretty(sum(pg_relation_size(idx))::bigint) as size, key, (array_agg(idx))[1] as idx1, (array_agg(idx))[2] as idx2 FROM ( ... ) sub GROUP BY key HAVING count(*) > 1 ORDER BY sum(pg_relation_size(idx)) DESC;
它过滤子查询以获取多次出现的键,获取索引的大小,并提供重复索引的名称。上面的示例只找到两个索引,但如果您怀疑它们可能有更多,只需添加 (array_agg(idx))[n] as idxn 即可获得更多索引。
我敢打赌您经常听说过constraint这个词,但它到底是什么?它是描述表中数据有效状态的条件(或规则)。约束与索引相关,因为每次创建约束时,也会创建一个索引(外键约束除外)。
您可以使用以下类型的constraint:
拥有适当的constraints 可以帮助您以正确的格式保存数据,还可以提高性能,因为您可以避免在数据库中包含无效信息,从而减少要处理、扫描或查询的数据。
最后但同样重要的是,了解有关索引的良好实践总是好的。不仅仅是因为技术面试!记住更一般的规则并在每次需要时更深入,而不是试图记住所有可用的信息,这样更容易。
尽量记住以下最佳实践,以尽可能高效地使用数据库:
当您养成索引列的习惯时,您将随着时间的推移生成自己的良好实践列表。此外,了解索引在幕后的工作方式将帮助您就数据库架构做出更自信的决策。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。