【问题标题】:Is cross-table indexing possible?可以跨表索引吗?
【发布时间】:2012-01-20 11:54:00
【问题描述】:

考虑一个结构,其中您与两个表上的条件(where、order by 等)具有多对一(或一对多)关系。例如:

CREATE TABLE tableTwo (
    id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    eventTime DATETIME NOT NULL,
    INDEX (eventTime)
) ENGINE=InnoDB;

CREATE TABLE tableOne (
    id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    tableTwoId INT UNSIGNED NOT NULL,
    objectId INT UNSIGNED NOT NULL,
    INDEX (objectID),
    FOREIGN KEY (tableTwoId) REFERENCES tableTwo (id)
) ENGINE=InnoDB;

对于示例查询:

select * from tableOne t1 
  inner join tableTwo t2 on t1.tableTwoId = t2.id
  where objectId = '..'
  order by eventTime;

假设您索引tableOne.objectIdtableTwo.eventTime。如果您随后对上述查询进行解释,它将显示“使用文件排序”。本质上,它首先应用tableOne.objectId 索引,但它不能应用tableTwo.eventTime 索引,因为该索引是针对整个tableTwo(不是有限的结果集),因此它必须进行手动排序。

因此,有没有办法进行跨表索引,这样就不必在每次检索结果时都进行文件排序?类似:

create index ind_t1oi_t2et on tableOne t1 
  inner join tableTwo t2 on t1.tableTwoId = t2.id 
  (t1.objectId, t2.eventTime);

另外,我已经研究过创建一个视图并为其编制索引,但视图不支持索引。

如果无法进行跨表索引,我一直倾向于的解决方案是将条件数据复制到一个表中。在这种情况下,这意味着eventTime 将在tableOne 中复制,并且将在tableOne.objectIdtableOne.eventTime 上设置多列索引(基本上是手动创建索引)。但是,我想我会先寻求其他人的经验,看看这是否是最好的方法。

非常感谢!

更新:

以下是加载测试数据和比较结果的一些程序:

drop procedure if exists populate_table_two;
delimiter #
create procedure populate_table_two(IN numRows int)
begin
declare v_counter int unsigned default 0;
  while v_counter < numRows do
    insert into tableTwo (eventTime) 
    values (CURRENT_TIMESTAMP - interval 0 + floor(0 + rand()*1000) minute);
    set v_counter=v_counter+1;
  end while;
end #
delimiter ;

drop procedure if exists populate_table_one;
delimiter #
create procedure populate_table_one
   (IN numRows int, IN maxTableTwoId int, IN maxObjectId int)
begin
declare v_counter int unsigned default 0;
  while v_counter < numRows do
    insert into tableOne (tableTwoId, objectId) 
      values (floor(1 +(rand() * maxTableTwoId)), 
              floor(1 +(rand() * maxObjectId)));
    set v_counter=v_counter+1;
  end while;
end #
delimiter ;

您可以使用这些来填充tableTwo 中的 10,000 行和 tableOne 中的 20,000 行(随机引用 tableOne 和随机 objectIds 介于 1 和 5 之间),这需要 26.2 和 70.77 秒分别为我跑:

call populate_table_two(10000);
call populate_table_one(20000, 10000, 5);

更新 2(测试触发 SQL):

下面是基于 daniHp 的触发方式的久经考验的 SQL。当添加tableOne 或更新tableTwo 时,这会使dateTimetableOne 保持同步。此外,如果将条件列复制到连接表,则此方法也适用于多对多关系。在我对tableOne 中的 300,000 行和tableTwo 中的 200,000 行的测试中,具有类似限制的旧查询的速度为 0.12 秒,而新查询的速度仍显示为 0.00 秒。因此,有了明显的改进,这种方法在数百万行甚至更远的行中应该都能很好地执行。

alter table tableOne add column tableTwo_eventTime datetime;

create index ind_t1_oid_t2et on tableOne (objectId, tableTwo_eventTime);

drop TRIGGER if exists t1_copy_t2_eventTime;
delimiter #
CREATE TRIGGER t1_copy_t2_eventTime
   BEFORE INSERT ON tableOne
for each row
begin
  set NEW.tableTwo_eventTime = (select eventTime 
       from tableTwo t2
       where t2.id = NEW.tableTwoId);
end #
delimiter ;

drop TRIGGER if exists upd_t1_copy_t2_eventTime;
delimiter #
CREATE TRIGGER upd_t1_copy_t2_eventTime
   BEFORE UPDATE ON tableTwo
for each row
begin
  update tableOne 
    set tableTwo_eventTime = NEW.eventTime 
    where tableTwoId = NEW.id;
end #
delimiter ;

以及更新后的查询:

select * from tableOne t1 
  inner join tableTwo t2 on t1.tableTwoId = t2.id
  where t1.objectId = 1
  order by t1.tableTwo_eventTime desc limit 0,10;

【问题讨论】:

  • 您可以创建另一个聚合表。
  • @anttir:有没有比在现有表之一中复制数据更可取的原因?
  • Sample code(此处为 SQL 形式)比 ad hoc 模式更有用。
  • @outis:谢谢,我会记住的。
  • This 问题让我思考同样的事情(顺便说一句,+1 表示致命问题)。就我个人而言,我认为如果 DDL 的功能大幅 增强(例如断言)达到跨表 CHECK 约束的程度,那就太好了。一个好的开始是能够将 SQL 放入 CHECK 约束中——目前唯一可以做到这一点的 RDBMS 是 Firebird(请参阅我的 answer 到一个有趣的相关问题)。

标签: mysql join indexing


【解决方案1】:

除了通过现已失效的 Akiban(?) 引擎之外,在 MySQL 中无法进行跨表索引。

我有一条规则:“不要规范化‘连续’值,例如 INT、FLOAT、DATETIME 等。”当您需要对连续值进行排序或范围测试时,JOIN 的成本会影响性能。

DATETIME 占用 5 个字节; INT 需要 4。因此,任何关于标准化日期时间的“空间”论点都相当糟糕。如果某个特定值的所有用途都发生变化,您很少需要“规范化”日期时间。

【讨论】:

  • 如果有时间可以给this question看看
  • @Rick James - mysql 新手,被规范化彻底烧毁(为了模型清洁,而不是出于任何空间原因)。你的规则现在铭刻在我的脑海里。 :-)
【解决方案2】:

可能是我错了,但如果这是我的应用程序,我不会复制数据,除非我需要在 2 个不同的表中按 2 列排序,这是一个热查询(需要多次)。但是由于没有明确的解决方案来避免filesort,那么这个小技巧呢(强制优化器使用eventTime子句的 order by 列上的索引)

select * from tableOne t1 
inner join tableTwo t2 use index (eventTime)  on t1.tableTwoId = t2.id and t2.eventTime > 0
where t1.objectId = 1
order by t2.eventTime desc limit 0,10;

注意use index (eventTime)t2.eventTime &gt; 0

解释表明优化器使用了 eventTime 上的索引而不是 filesort

1   SIMPLE  t2  range   eventTime   eventTime   5       5000    Using where; Using index
1   SIMPLE  t1  ref objectId,tableTwoId tableTwoId  4   tests.t2.id 1   Using where

【讨论】:

  • 您已经欺骗优化器从 T2 开始,以便它可以避免排序。但是...如果只有很少的行有 t1.objectId = 1,那么可能需要很长时间才能找到匹配的 10 行。此外,如果只有“旧”行有“1”,则可能需要很长时间才能找到 10。底线:kludge 可能会有所帮助,但可能会造成伤害。 STRAIGHT_JOIN(和一些重写)是一种更简单的强制 T2 先被击中的方法。但是,出于同样的原因,我从不推荐它。
  • 遇到了一个非常相似的问题,并了解了强制索引使用时间的不同方法,只是为了准确地实现您在此处指出的内容。
【解决方案3】:

如您所知,SQLServer 通过indexed views 实现了这一点:

索引视图提供了无法实现的额外性能优势 使用标准指标实现。索引视图可以增加查询 表现在以下方面:

聚合可以预先计算并存储在索引中以最小化 查询执行期间的昂贵计算。

可以预先加入表并存储结果数据集。

可以存储连接或聚合的组合。

在 SQLServer 中,要利用此技术,您必须查询视图而不是表。这意味着您应该了解视图和索引。

MySQL 没有索引视图,但您可以用表 + 触发器 + 索引模拟行为

您必须创建一个索引表,一个使数据表保持最新的触发器,而不是创建视图,然后您必须查询新表而不是规范化表。

您必须评估写入操作的开销是否抵消了读取操作的改进。

已编辑:

请注意,并不总是需要创建新表。例如,在 1:N 关系(主-详细信息)触发器中,您可以将“主”表中的字段副本保留到“详细信息”表中。在你的情况下:

CREATE TABLE tableOne (
    id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    tableTwoId INT UNSIGNED NOT NULL,
    objectId INT UNSIGNED NOT NULL,
    desnormalized_eventTime DATETIME NOT NULL,
    INDEX (objectID),
    FOREIGN KEY (tableTwoId) REFERENCES tableTwo (id)
) ENGINE=InnoDB;

CREATE TRIGGER tableOne_desnormalized_eventTime
   BEFORE INSERT ON tableOne
for each row
begin
  DECLARE eventTime DATETIME;
  SET eventTime = 
      (select eventTime 
       from tableOne
       where tableOne.id = NEW.tableTwoId);
  NEW.desnormalized_eventTime = eventTime;
end;

注意这是一个插入前触发器。

现在,查询重写如下:

select * from tableOne t1 
  inner join tableTwo t2 on t1.tableTwoId = t2.id
  where t1.objectId = '..'
  order by t1.desnormalized_eventTime;

免责声明:未经测试。

【讨论】:

  • +1:我喜欢使用触发器复制索引数据的想法!我可能会采用这种方法,但将eventTime 直接添加到tableOne,因为这样可以最大限度地减少解决方案所需的复制、代码重写和额外的内存消耗。
  • 不错。如果是数据库维护复制数据,他们是不会忘记更新的。目前,我正在使用 ORM (django),并将这种代码保存在 save() 方法中(对象持久性)。出于标准化原因,我犹豫是否这样做,但我很高兴复制数据。对于“学术开发人员”来说,这是一个艰难的决定;)
  • 注意:对于使用此解决方案的用户,如果eventTime 未修复,请务必在tableTwo 上添加更新触发器。
  • 警告:触发器不是“免费的”。这种组合的成本可能比收益更糟糕。
猜你喜欢
  • 1970-01-01
  • 2020-11-11
  • 1970-01-01
  • 2022-12-29
  • 2021-12-24
  • 2020-10-03
  • 1970-01-01
  • 2013-01-27
  • 1970-01-01
相关资源
最近更新 更多