是否可以执行通常的原子插入操作但异步更新索引?

Is it possible to do usual atomic INSERT operation but update Indexes asynchronously?

索引使读快写慢。但是,为什么不能进行单次写入并让 db 随时间异步添加索引,还在 INSERT 中缓存直到它被索引?

有这样的数据库吗?

将我的评论转换为答案:

indexes make read fast but write slower

这是一种过于简单化的说法,而且还具有误导性。

索引使数据 查找 更快,因为 DBMS 不需要执行 table-scan 来查找与谓词匹配的行(WHERE 部分一个问题)。索引不会使“读取”速度更快(这完全取决于磁盘 IO 的特性),如果使用不当,它们有时甚至会使查询变慢(原因我不会深入)。

我想强调的是,在执行 DML 语句 (INSERT/UPDATE/DELETE/MERGE/etc) 时,写入单个索引甚至多个索引的额外成本可以忽略不计,真的! (实际上:foreign-key 约束是一个更大的罪魁祸首 - 我注意到您实际上可以通过添加额外的索引 来消除 foreign-key 约束检查 的成本!)。索引主要使用 B-trees 实现(B-tree 本质上类似于 binary-tree,除了每个节点只有 2 个子节点,它可以有 多个 children 因为每个 tree-node 都带有未使用的 space 用于所有这些子节点指针,因此插入 B-tree 的中间不需要数据 moved-around on-disk 不像其他种类的树,像 heap-tree).

Consider this QA where a Postgres user (like yourself) reports inserting 10,000 rows into a table. Without an index it took 78ms, with an index it took 84ms,这仅增加了 7.5%,在那个规模(6 毫秒!)下,它是如此之小,也可能是舍入错误或由 IO 调度引起的。如果没有实际的硬数据显示这对您和您的应用程序来说是一个问题,那应该足以证明它不应该是您应该担心的事情。

我假设您对索引 after reading an article like this one 有这种负面印象,这肯定会给人一种“索引不好”的印象——但是尽管那篇文章中提到的观点并没有错,但有一个 很多 那篇文章的问题,所以你不应该教条地接受它。 (我会在页脚列出我对该文章的担忧)。

But why can't you have single writes and have db add indexes asynchronously with time

我假设您的意思是您希望 DMBS 通过简单地将新记录附加到 table 的末尾来执行 single-row INSERT,然后立即 returning 然后在任意时间之后,DBMS 的内务管理系统将更新索引。

问题在于它破坏了 the A.C.I.D. model 的 A、C 和 I 部分。

索引不仅仅用于避免 table-scans:它们还用于存储 table 数据的副本,以便查询使用索引并且还需要 (例如)table 数据的一小部分,这会显着减少磁盘读取。因此,RDBMS(和 ISO SQL)允许索引使用 INCLUDES 子句包含 non-indexed 数据。

考虑这种情况:

CREATE INDEX IX_Owners ON cars ( ownerId ) INCLUDE ( colour );
CREATE INDEX IX_Names  ON people ( name ) INCLUDE ( personId, hairColour );

GO;

SELECT
    people.name,
    people.hairColour,
    cars.colour
FROM
    cars
    INNER JOIN people ON people.personId = cars.ownerId
WHERE
    people.name LIKE 'Steve%'

以上查询 不需要 读取 carspeople tables on-disk。 DBMS 将能够仅使用索引中的数据来完全回答查询——这很好,因为索引往往存在于少量 on-disk 中,而这些页往往是在 proximal-locality 这对性能有好处,因为这意味着它将使用 顺序 IO 随机 IO.

RDBMS 将执行 people.IX_Names 索引的 string-prefix index-scan 以获取所有 personId(和 hairColour)值,然后它将 look-up cars.IX_Owners 索引中的那些 personId 值,并能够从 IX_Owners 索引中的数据副本中获取 car.colour 而无需读取table 直接。


现在,假设另一个数据库客户端刚刚完成插入记录负载到 cars and/or people table 和 COMMIT TRANSACTION为了更好地衡量,RDMBS 会使用您的想法,以后 只要感觉需要就只更新索引 ,那么如果同一个数据库客户端 re-runs 来自上面的查询将 return过时的数据(即错误的数据)因为查询使用了索引,但是索引是旧的。


除了使用索引树节点来存储table数据的副本以避免non-proximal磁盘IO,许多RDBMS还使用index-trees来存储整个副本 - 甚至 table 数据的多个副本,以启用其他场景,例如 columnar data storage and indexed-VIEWs - 这两个功能都绝对需要使用 table 数据自动更新索引。

Is there any database like that?

是的,它们存在 - 但它们没有被广泛使用(或者它们是小众的),因为出于上述原因,对于绝大多数应用程序来说,这是完全不受欢迎的行为。

分布式数据库是围绕最终一致性设计的,但是客户端(和整个应用程序代码)需要设计成这样in-mind,必须重新设计 data-centric 应用程序以支持 eventual-consistency 是一个巨大的 PITA,这就是为什么你只真正看到它们被用于真正的大型系统(如 Facebook,Google, 等)可用性(正常运行时间)比用户在几米内看到 stale-data 更重要坚果。


脚注:

关于这篇文章:https://use-the-index-luke.com/sql/dml/insert

The number of indexes on a table is the most dominant factor for insert performance. The more indexes a table has, the slower the execution becomes. The insert statement is the only operation that cannot directly benefit from indexing because it has no where clause.

我不同意。我认为 foreign-key 约束(和触发器)远 更有可能 对 DML 操作产生更大的不利影响。

Adding a new row to a table involves several steps. First, the database must find a place to store the row. For a regular heap table—which has no particular row order—the database can take any table block that has enough free space. This is a very simple and quick process, mostly executed in main memory. All the database has to do afterwards is to add the new entry to the respective data block.

我同意这一点。

If there are indexes on the table, the database must make sure the new entry is also found via these indexes. For this reason it has to add the new entry to each and every index on that table. The number of indexes is therefore a multiplier for the cost of an insert statement.

这是事实,但我不知道我是否同意这是插入成本的“乘数”。

例如,考虑具有数百个 nvarchar(1000) 列和多个 int 列的 table - 每个 int 列都有单独的索引(没有 INCLUDE 列)。如果您要插入 100x megabyte-sized 行 all-at-once(使用 INSERT INTO ... SELECT FROM 语句),更新这些 int 索引的成本很可能需要比 table数据。

Moreover, adding an entry to an index is much more expensive than inserting one into a heap structure because the database has to keep the index order and tree balance. That means the new entry cannot be written to any block—it belongs to a specific leaf node. Although the database uses the index tree itself to find the correct leaf node, it still has to read a few index blocks for the tree traversal.

我非常不同意这一点,尤其是第一句话:“向索引添加条目比向堆结构插入条目要昂贵得多”。

今天 RDBMS 中的索引总是基于 B-trees, 而不是 binary-trees 或 heap-trees。 B-trees 本质上类似于 binary-trees 除了每个节点都有 built-in space 用于数十个子节点指针并且 B-trees 仅在节点填充其内部子节点时才重新平衡指针列表,因此 B-tree 节点插入将比文章所说的便宜得多,因为每个节点将有大量空 space 用于新插入而不需要 re-balance 本身或任何其他相对昂贵的操作(此外,任何 DML 语句的 DBMS can and do index maintenance separately and independently)。

文章 关于 DBMS 需要如何遍历 B-tree 来找到要插入的节点是正确的,但是索引节点是有效排列的 on-disk,例如将相关节点保持在同一磁盘页面中,从而最大限度地减少索引 IO 读取(假设它们尚未首先加载到内存中)。如果索引树太大而无法存储 in-memory,RDBMS 可以始终保留“meta-indexes”in-memory,因此它可能 立即 找到正确的B-tree 不需要从根开始遍历 B-tree 的索引。

Once the correct leaf node has been identified, the database confirms that there is enough free space left in this node. If not, the database splits the leaf node and distributes the entries between the old and a new node. This process also affects the reference in the corresponding branch node as that must be duplicated as well. Needless to say, the branch node can run out of space as well so it might have to be split too. In the worst case, the database has to split all nodes up to the root node. This is the only case in which the tree gains an additional layer and grows in depth.

实际上这不是问题,因为 RDBMS 的索引维护将确保每个索引节点中有足够的空闲 space。

The index maintenance is, after all, the most expensive part of the insert operation. That is also visible in Figure 8.1, “Insert Performance by Number of Indexes”: the execution time is hardly visible if the table does not have any indexes. Nevertheless, adding a single index is enough to increase the execute time by a factor of a hundred. Each additional index slows the execution down further.

我认为这篇文章暗示(暗示?陈述?)每个 DML 都会发生 index-maintenance,这是不诚实的。这不是真的。一些早期的 dBase-era 数据库可能就是这种情况,但对于 Postgres、MS SQL Server、Oracle 等现代 RDBMS 肯定不是这种情况。

Considering insert statements only, it would be best to avoid indexes entirely—this yields by far the best insert performance.

同样,文章 中的这个说法并没有错,但它基本上是说,如果你想要一个干净整洁的房子,你应该摆脱你所有的财产。索引是生活中的事实。

However tables without indexes are rather unrealistic in real world applications. You usually want to retrieve the stored data again so that you need indexes to improve query speed. Even write-only log tables often have a primary key and a respective index.

确实如此。

Nevertheless, the performance without indexes is so good that it can make sense to temporarily drop all indexes while loading large amounts of data—provided the indexes are not needed by any other SQL statements in the meantime. This can unleash a dramatic speed-up which is visible in the chart and is, in fact, a common practice in data warehouses.

同样,对于现代 RDBMS,这不是必需的。如果您执行批量插入,那么 RDBMS 将不会更新索引,直到 after table-data 完成修改,因为批量索引更新比许多单独更新便宜。同样,我 预计 显式 BEGIN TRANSACTION 中的多个 DML 语句和查询可能 导致 index-update 延迟,前提是没有后续查询在事务中依赖于更新的索引。

但我对那篇文章最大的问题是,作者在没有提供任何引用甚至他们自己 运行 的基准的情况下做出这些关于有害 IO 性能的大胆声明.更令人恼火的是,他们再次张贴了带有任意数字的 bar-chart,没有任何引用或原始基准数据以及如何重现其结果的说明。始终要求从您阅读的任何内容中引用和证明声明:因为任何人在没有证据的情况下应该接受的唯一声明是逻辑公理 - 而关于数据库索引 IO 成本的定量声明不是逻辑公理 :)

对于 PostgreSQL GIN 索引,有快速更新功能。这会将新的索引条目存储到一个无序的未合并区域中,等待其他进程将它们归档到主索引结构中。但这并不直接符合你想要的。它的设计主要是为了让索引更新批量完成(这可以提高 IO 效率),而不是在后台完成。一旦未合并的区域变得足够大,那么前台进程可能会承担将它们归档的任务,并且很难调整设置以使其始终由后台进程而不是前台进程完成.而且它只适用于 GIN 索引。 (使用 btree_gin 扩展,您可以在常规标量列上创建 GIN 索引,而不是它通常使用的 array-like 列。)在等待合并条目时,每个查询都会有顺序扫描未合并的缓冲区,因此为了 INSERT 而延迟更新可能会给 SELECT 带来高昂的成本。

有更通用的技术可以做这样的事情,例如 fractal tree indexes。但是这些都没有在 PostgreSQL 中实现,而且无论在哪里实现,它们似乎都是专有的。