【问题标题】:mariadb not using all fields of composite indexmariadb 没有使用复合索引的所有字段
【发布时间】:2021-10-29 01:32:50
【问题描述】:

Mariadb 没有完全使用复合索引。快选和慢选都返回相同的数据,但是解释表明慢选只使用了 ix_test_relation.entity_id 部分,不使用 ix_test_relation.stamp 部分。

我尝试了很多情况(内连接、with、from),但无法让 mariadb 将索引的两个字段与递归查询一起使用。我知道我需要告诉 mariadb 以某种方式实现递归查询。

请帮助我优化使用递归查询的慢速选择,使其速度与快速选择相似。

关于任务的一些细节...我需要查询用户活动。一个用户活动记录可能涉及多个实体。实体是分层的。我需要查询某些父实体的用户活动和指定邮票范围的所有子实体。为简化演示,Stamp 从 TIMESTAMP 简化为 BIGINT。可能有很多(100 万)个实体,每个实体可能与很多(100 万)个用户活动条目相关。实体层次结构深度预计为 10 级深。我假设使用的标记范围将用户活动记录的数量减少到 10-100。我对模式进行了非规范化,将标记从 test_entry 复制到 test_relation 以便能够将其包含在 test_relation 索引中。

我使用 10.4.11-Mariadb-1:10:4.11+maria~bionic。 如果需要,我可以升级或修补或任何 mariadb,我可以完全控制构建 docker 映像。

架构:

CREATE TABLE test_entity(
  id BIGINT NOT NULL,
  parent_id BIGINT NULL,
  CONSTRAINT pk_test_entity PRIMARY KEY (id),
  CONSTRAINT fk_test_entity_pid FOREIGN KEY (parent_id) REFERENCES test_entity(id)
);
CREATE TABLE test_entry(
  id BIGINT NOT NULL,
  name VARCHAR(100) NOT NULL,
  stamp BIGINT NOT NULL,
  CONSTRAINT pk_test_entry PRIMARY KEY (id)
);
CREATE TABLE test_relation(
  entry_id BIGINT NOT NULL,
  entity_id BIGINT NOT NULL,
  stamp BIGINT NOT NULL,
  CONSTRAINT pk_test_relation PRIMARY KEY (entry_id, entity_id),
  CONSTRAINT fk_test_relation_erid FOREIGN KEY (entry_id) REFERENCES test_entry(id),
  CONSTRAINT fk_test_relation_enid FOREIGN KEY (entity_id) REFERENCES test_entity(id)
);
CREATE INDEX ix_test_relation ON test_relation(entity_id, stamp);
CREATE SEQUENCE sq_test_entry;

测试数据:

CREATE OR REPLACE PROCEDURE test_insert()
BEGIN
  DECLARE v_entry_id BIGINT;
  DECLARE v_parent_entity_id BIGINT;
  DECLARE v_child_entity_id BIGINT;
  FOR i IN 1..1000 DO
    SET v_parent_entity_id = i * 2;
    SET v_child_entity_id = i * 2 + 1;
    INSERT INTO test_entity(id, parent_id)
    VALUES(v_parent_entity_id, NULL);
    INSERT INTO test_entity(id, parent_id)
    VALUES(v_child_entity_id, v_parent_entity_id);
    FOR j IN 1..1000000 DO
      SELECT NEXT VALUE FOR sq_test_entry
      INTO v_entry_id;
      INSERT INTO test_entry(id, name, stamp)
      VALUES(v_entry_id, CONCAT('entry ', v_entry_id), j);
      INSERT INTO test_relation(entry_id, entity_id, stamp)
      VALUES(v_entry_id, v_parent_entity_id, j);
      INSERT INTO test_relation(entry_id, entity_id, stamp)
      VALUES(v_entry_id, v_child_entity_id, j);
    END FOR;
  END FOR;
END;
CALL test_insert;

慢速选择(> 100ms):

SELECT entry_id
FROM test_relation TR
WHERE TR.entity_id IN (
  WITH RECURSIVE recursive_child AS (
    SELECT id
    FROM test_entity
    WHERE id IN (2, 4)
    
    UNION ALL
    
    SELECT C.id
    FROM test_entity C
    INNER JOIN recursive_child P
    ON P.id = C.parent_id
  )
  SELECT id
  FROM recursive_child
)
AND TR.stamp BETWEEN 6 AND 8

快速选择(1-2ms):

SELECT entry_id
FROM test_relation TR
WHERE TR.entity_id IN (2,3,4,5)
AND TR.stamp BETWEEN 6 AND 8

更新 1

我可以用更短的例子来证明这个问题。

在临时表中显式存储所需的 entity_id 记录

CREATE OR REPLACE TEMPORARY TABLE tbl
WITH RECURSIVE recursive_child AS (
  SELECT id
  FROM test_entity
  WHERE id IN (2, 4)
    
  UNION ALL
    
  SELECT C.id
  FROM test_entity C
  INNER JOIN recursive_child P
  ON P.id = C.parent_id
)
SELECT id
FROM recursive_child

尝试使用临时表(如下)运行选择。 Select 仍然很慢,但现在与快速查询的唯一区别是 IN 语句查询表而不是内联常量。

SELECT entry_id
FROM test_relation TR
WHERE TR.entity_id IN (SELECT id FROM tbl)
AND TR.stamp BETWEEN 6 AND 8

【问题讨论】:

  • 我考虑过将复合索引切换为 stamp,entity_id,但在这种情况下,相对于实体数量而言,索引对应于 O(n) 而不是 O(1)。这意味着此查询会随着实体的增加而变慢,这不应该发生
  • 请提供EXPLAIN SELECT ...
  • 如果 stampTIMESTAMP,请保持原样。 BETWEEN 6 AND 8 闻起来像 IN(6,7,8)TIMESTAMP 不太可能发生这种情况。
  • 拥有 both (stamp, entity_id) 和 (entity_id, stamp) 不会有什么坏处——这样优化器可以动态地选择它们之间基于数据集。

标签: indexing mariadb query-optimization sql-execution-plan composite-key


【解决方案1】:

对于您的查询(两者),在我看来,正如您所提到的,您应该翻转复合索引上的列顺序:

CREATE INDEX ix_test_relation ON test_relation(stamp, entity_id);

为什么?

您的查询在该列上有一个范围过滤器TR.stamp BETWEEN 2 AND 3。对于使用索引范围扫描的范围过滤器(无论是在 TIMESTAMPBIGINT 列上),被过滤的列必须在多列索引中首先

您还想要一个sargable 过滤器,就像这样:

    TR.stamp >= CURDATE() - INTERVAL 7 DAY
AND TR.stamp <  CURDATE()

代替

    DATE(TR.stamp) BETWEEN DATE(NOW() - INTERVAL 7 DAY) AND DATE(NOW())

也就是说,不要在 WHERE 子句中正在扫描的列上放置函数。

对于第一个查询这样的结构化查询,查询规划器会将其转换为多个查询。您可以通过ANALYZE FORMAT=JSON 看到这一点。规划器可以为每个子查询选择不同的索引和/或不同的索引块。

而且,对聪明人说一句:不要太绕轴试图猜出 DBMS 中内置的查询规划器。这是一款极其复杂且高度精炼的软件,由世界级优化专家数十年的编程工作创建。作为 MariaDB / MySQL 用户,我们的工作是找到正确的索引。

【讨论】:

  • 正如我提到的 (stamp,entity_id) 索引的问题在于它是 O(n),而不是 O(1)。基本上,您在指定日期范围内查询所有实体的用户活动。问题是当实体数量增加时,这种索引的使用会降低性能。假设我们有 100 万个实体,每个实体在上周生成 3 条用户活动记录。要获取 3 个实体的 9 条记录,数据库将扫描此索引中的 3 百万条记录。
  • 我不能忽略这个问题,因为我需要使用使用 1% 的 DBMS 的代码来实现要求,并且运行时间为毫秒,而 100% 的 CPU 和几秒钟的一个糟糕的用户活动查询甚至不是关键业务
  • 关于 sargabling... 我很清楚这个问题,你可能会看到我的示例中没有任何函数或其他类型的转换。我使用 BIGINT 字段类型只是为了简化示例。我将时间戳(3)字段用于我的真实代码
【解决方案2】:

复合索引中列的顺序很重要。 (O.Jones 很好地解释了它——使用已从问题中删除的 SQL?!)

我会重写

SELECT entry_id
FROM test_relation TR
WHERE TR.entity_id IN (SELECT id FROM tbl)
AND TR.stamp BETWEEN 6 AND 8

作为

SELECT TR.entry_id
    FROM tbl
    JOIN test_relation TR  ON tbl.id = TR.entity_id
    WHERE TR.stamp BETWEEN 6 AND 8

SELECT entry_id
    FROM test_relation TR
    WHERE TR.stamp BETWEEN 6 AND 8
      AND EXISTS ( SELECT 1 FROM tbl
                       WHERE tbl.id = TR.entity_id )

在任何一种情况下都有这些:

TR: INDEX(stamp, entity_id, entry_id)  -- With `stamp` first
tbl: INDEX(id) -- maybe

由于tbl是新建的TEMPORARY TABLE,而且好像只需要检查3行,可能不值得添加INDEX(id)

还需要:

test_entity:  INDEX(parent_id, id)

假设test_relation 是一个many:many 映射表,您可能还需要(尽管对于当前查询而言不一定):

INDEX(entity_id, entry_id)

【讨论】:

  • 我试过了,没用。你可以自己试试看
  • @user1194528 - 请提供“不起作用”的确切 SHOW CREATE TABLE。我没有提到的一条规则:如果你有 both INDEX(a)INDEX(a,b),优化器 可能 选择前者,而后者实际上会更好。所以,放弃前者。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-01-13
  • 2012-10-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多