【问题标题】:Update mysql big table hang too time更新mysql大表挂了太时间
【发布时间】:2023-03-31 12:52:01
【问题描述】:

更新MySql MyISAM大表根据同一张表上的索引制作列升序的性能问题

我的问题是服务器只有 4 GB 内存。
我必须进行这样的更新查询:previous asked question
我的是这个:

set @orderid = 0;  
update images im
    set im.orderid = (select @orderid := @orderid + 1) 
    ORDER BY im.hotel_id, im.idImageType;

im.hotel_id, im.idImageType 我有一个升序索引。
im.orderid 我也有一个升序索引。

该表有 2100 万条记录,并且是一个 MyIsam 表。

表格是这样的:

CREATE TABLE `images` (
`photo_id` int(11) NOT NULL,
`idImageType` int(11) NOT NULL,
`hotel_id` int(11) NOT NULL,
`room_id` int(11) DEFAULT NULL,
`url_original` varchar(150) COLLATE utf8_unicode_ci NOT NULL,
`url_max300` varchar(150) COLLATE utf8_unicode_ci NOT NULL,
`url_square60` varchar(150) COLLATE utf8_unicode_ci NOT NULL,
`archive` int(11) NOT NULL DEFAULT '0',
`orderid` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`photo_id`),
KEY `idImageType` (`idImageType`),
KEY `hotel_id` (`hotel_id`),
KEY `hotel_id_idImageType` (`hotel_id`,`idImageType`),
KEY `archive` (`archive`),
KEY `room_id` (`room_id`),
KEY `orderid` (`orderid`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

问题在于性能:挂了几分钟!
服务器磁盘也很忙。

我的问题是:有更好的方法来达到相同的结果吗?
我是否需要对表进行分区或其他什么来提高性能?
我无法修改服务器硬件,但可以调整 MySql 应用程序数据库服务器设置。

最好的问候

【问题讨论】:

    标签: mysql sql performance myisam


    【解决方案1】:

    坦克到每个身体。你的回答对我帮助很大。我认为现在我找到了更好的解决方案。

    这个问题涉及两个关键问题:

    • 在大表上高效分页
    • 更新大表。

    为了在大表上进行有效的分页,我通过在表上进行先前更新找到了解决方案,但这样做我遇到了更新所需的 51 分钟时间问题,因此我的 java 基础架构超时(spring-批处理步骤)。

    现在在您的帮助下,我找到了两种在大表上分页的解决方案,以及一种更新大表的解决方案。
    为了达到这个性能,服务器需要内存。我在使用 32 GB 内存的开发服务器上尝试此解决方案。

    常见的解决步骤

    要按照我需要的字段 tupla 进行分页,我已经创建了一个索引:

    KEY `hotel_id_idImageType` (`hotel_id`,`idImageType`) 
    

    要实现新的解决方案,我们必须通过将主键部分添加到索引尾部KEY hotel_id_idImageType (hotel_id,idImageType, primary key fields)来更改此索引:

    drop index hotel_id_idImageType on images;
    create index hotelTypePhoto on images (hotel_id, idImageType, photo_id);  
    

    这是避免触摸表并仅使用索引文件所必需的...

    假设我们想要 19000000 条记录之后的 10 条记录。

    这个答案中的小数点是,

    解决方案 1

    此解决方案非常实用,不需要额外的字段orderid,并且您不必在分页之前进行任何更新:

    select * from images im inner join 
      (select photo_id from images 
      order by hotel_id, idImageType, photo_id 
      limit 19000000,10) k 
    on im.photo_id = k.photo_id;  
    

    在我的 2100 万表记录上创建表 k 只需要 1.5 秒,因为它只使用索引 hotelTypePhoto 中的三个字段,所以还没有访问表文件并仅处理索引文件。

    订单与原始所需的 (hotel_id, idImageType) 一样,因为包含在 (hotel_id, idImageType, photo_id):同一个子集...

    加入不需要时间,因此每次在同一页面上执行分页只需要 1.5 秒,如果您必须在 3 个月内分批执行,这是一个好时机。

    在使用 4 GB 内存的生产服务器上,相同的查询需要 3.5 秒。

    对表进行分区无助于提高性能。

    如果服务器将其放入缓存中,时间会减少,或者如果您执行 jdbc 参数声明,时间也会减少(我想)。

    如果你必须经常使用它,它的优点是它不关心数据是否发生变化。

    解决方案 2

    这个方案需要额外的字段orderid,并且需要批量导入更新一次orderid,直到下一次批量导入数据才可以改变。

    然后你可以在 0000 秒内对表格进行分页。

    set @orderid = 0;  
    update images im inner join (
      select photo_id, (@orderid := @orderid + 1) as newOrder 
      from images order by hotel_id, idImageType, photo_id
    ) k
    on im.photo_id = k.photo_id
    set im.orderid = k.newOrder;  
    

    表 k 几乎和第一个解决方案一样快。

    所有更新只需要 150,551 秒,比 51 分钟要好得多!!! (150s vs 3060s)

    在批量更新后,您可以通过以下方式进行分页:

     select * from images im where orderid between 19000000 and 19000010;
    

    或更好

     select * from images im where orderid >= 19000000 and orderid< 19000010;  
    

    这需要 0,000 秒来执行第一次和所有其他时间。

    Rick 评论后编辑

    解决方案 3

    此解决方案是为了避免使用额外的字段和偏移量。但也需要像this solution中那样记住最后一页的读取

    这是一种快速的解决方案,仅使用 4GB 内存即可用于在线服务器生产

    假设您需要读取 20000000 之后的最后十条记录。
    有两种情况需要注意:

    • 如果您像我一样需要全部内容,可以从第一页开始读取到 20000000,并更新一些变量以记录最后一页读取的内容。
    • 您只需要读取 20000000 之后的最后 10 个。

    在第二种情况下,您必须进行预查询才能找到起始页:

    select hotel_id, idImageType, photo_id 
      from images im 
      order by hotel_id, idImageType, photo_id limit 20000000,1
    

    它给了我:

    +----------+-------------+----------+
    | hotel_id | idImageType | photo_id |
    +----------+-------------+----------+
    |  1309878 |           4 | 43259857 |
    +----------+-------------+----------+
    

    这需要 6.73 秒。
    因此,您可以将此值存储在变量中以供下次使用。
    假设我们命名为@hot=1309878, @type=4, @photo=43259857 然后你可以像这样在第二个查询中使用它:

    select * from images im  
      where  
      hotel_id>@hot OR (
        hotel_id=@hot and idImageType>@type OR (
         idImageType=@type and photo_id>@photo
        )
      )  
      order by hotel_id, idImageType, photo_id limit 10;  
    

    第一个子句hotel_id&gt;@hot 获取滚动索引上实际第一个字段之后的所有记录,但丢失了一些记录。要获取它,我们必须执行 OR 子句,该子句将第一个索引字段全部保留为未读记录。

    现在只需 0.10 秒。
    但是这个查询可以优化(布尔分布):

    select * from images im  
      where  
      hotel_id>@hot OR (
        hotel_id=@hot and 
         (idImageType>@type or idImageType=@type) 
         and (idImageType>@type or photo_id>@photo
        )
      )  
      order by hotel_id, idImageType, photo_id limit 10;  
    

    变成:

    select * from images im  
      where  
      hotel_id>@hot OR (
        hotel_id=@hot and 
         idImageType>=@type
         and (idImageType>@type or photo_id>@photo
        )
      )  
      order by hotel_id, idImageType, photo_id limit 10;  
    

    变成:

    select * from images im  
      where  
      (hotel_id>@hot OR hotel_id=@hot) and 
      (hotel_id>@hot OR
         (idImageType>=@type and (idImageType>@type or photo_id>@photo))
      )
      order by hotel_id, idImageType, photo_id limit 10;  
    

    变成:

    select * from images im  
      where  
      hotel_id>=@hot and 
      (hotel_id>@hot OR
         (idImageType>=@type and (idImageType>@type or photo_id>@photo))
      )
      order by hotel_id, idImageType, photo_id limit 10;  
    

    它们是我们可以通过限制获得的相同数据吗?

    要快速不详尽测试:

    select im.* from images im inner join (
      select photo_id from images order by hotel_id, idImageType, photo_id limit 20000000,10
    ) k 
    on im.photo_id=k.photo_id 
    order by im.hotel_id, im.idImageType, im.photo_id;
    

    这需要 6.56 秒,数据与上面的查询相同。
    所以测试是阳性的。

    在此解决方案中,您只需要在第一次需要在第一页上寻找阅读时花费 6.73 秒(但如果您需要所有内容,则不需要)。

    要真正的所有其他页面,您只需要 0.10 秒即可获得非常好的结果。

    感谢 rick 对基于存储最后一页读取的解决方案的提示。

    结论

    解决方案 1 上,您没有任何额外的字段,每页需要 3.5 秒
    解决方案 2 上,您有额外的字段,需要一个大内存服务器(测试 32 GB)在 150 秒内。但是您在 0,000 秒内阅读了该页面。
    解决方案 3 上,您没有任何额外的字段,但必须存储最后一页读取指针,如果您没有从第一页开始阅读,则必须花费 6,73第一页秒。然后你在所有其他页面上只花费 0.10 秒。

    最好的问候

    编辑 3

    解决方案 3 正是 Rick 所建议的。对不起,在我之前的解决方案 3 中我犯了一个错误,当我编写了正确的解决方案时,我应用了一些布尔规则,如分配属性等,之后所有我得到相同的丰富解决方案! 问候

    【讨论】:

    • This 讨论如何在不添加列和不使用“偏移”的情况下获得解决方案 2 的速度。
    • “This”有一个关于分块删除的链接。
    • 您无法使用以下简单代码继续“从上次中断的地方”使用复合索引:hotel_id&gt;=1309878 and idImageType&gt;=4 and photo_id&gt;=43259857。它涉及几个 AND 和 OR。请参阅我的答案中的示例代码。
    • 原谅我糟糕的英语。我可以将 1309878、4、43259857 存储在 vars 中并在下一页中使用。 hotel_id&gt;=1309878 and idImageType&gt;=4 and photo_id&gt;=43259857 挂钩索引,也按挂钩索引排序。证明是 0,12 秒的执行时间。当我阅读最后记录的页面时,我存储 (hotel_id,idImageType,photo_id) tupla 以在下一页中使用。在我的代码中我没有OR。抱歉,我不明白您删除块的链接。
    • 是存储两个数字,但不,where 子句不正确。请参阅我的回答中的WHERE。 (但是,我的回答假设您有 3 个变量。)
    【解决方案2】:

    您可以使用其中的一些:

    1. 更新引擎到 InnoDB,它只阻塞一行,而不是更新时的所有表。

    2. 使用 photo_id 和良好的 orderid 创建 #temp 表,然后从此 temp 更新您的表:

      update images im, temp tp
      set im.orderid = tp.orderid
      where im.photo_id = tp.photo_id
      

    这将是最快的方式,当您填充 tmp 表时 - 您在主表上没有任何块。

    1. 您可以在批量更新之前删除索引。在您进行所有单次更新后,您已经重建了索引,而且需要很长时间。

    【讨论】:

    • 感谢您的回答。我尝试这样做,但效果不佳。
    • 最后一条评论被中继了。谢谢你的回答。我试着去做,但没有更好地工作。如果我使用第一个查询,则需要 51 分钟。在您的解决方案中,制作临时表需要 2.5 分钟,但是当我进行更新时,它会超过 60 分钟(在 60 分钟时我会杀了它)。我尝试使用内部连接重写更新,但也需要超过 60 分钟。虽然我没有删除索引。
    • 两张表中的 photo_id 都有索引吗?该索引是唯一的(主要的)吗?
    • 嗯...尝试删除所有索引,只保留主键并检查更新时间。创建一个单独的小表并检查查询的解释。
    • 检查mysqltuner.pl它会检查您的服务器统计信息并建议更改参数
    【解决方案3】:
    KEY `hotel_id`             (`hotel_id`),
    KEY `hotel_id_idImageType` (`hotel_id`,`idImageType`),
    

    删除前者;后者可以满足任何需要。 (这不会加快原始查询的速度。)

    “问题在于性能:挂几分钟!”有什么问题?

    • 其他查询被阻止几分钟? (InnoDB 应该会有所帮助。)
    • 您经常运行此更新并且很烦人? (为什么在世界上??)
    • 还有别的吗?

    在进行更新时,这一索引成本很高:

    KEY `orderid` (`orderid`)
    

    删除它并重新创建它。 (不要费心放弃其余的。)使用 InnoDB 的另一个原因是这些操作可以在不复制表的情况下完成(在 5.6 中)。 (21M 行 == 如果必须复制表,则需要很长时间!)

    除了已经是唯一的 photo_id 之外,您为什么还要构建第二个唯一索引 (orderid)?我问这个是因为可能有另一种方法可以解决真正的问题,而不涉及这个耗时的更新。

    我还有两个更具体的建议,但我想先在这里回答你。

    编辑分页,按hotel_id, idImageType, photo_id排序:

    可以按该三元组的顺序读取记录。甚至可以通过它们“分页”。

    如果您在 ($hid, $type, $pid) 之后“停止”,则这里将是“下一个”20 条记录:

    WHERE   hotel_id >= $hid
      AND ( hotel_id >  $hid
         OR       idImageType >= $type
            AND ( idImageType >  $type
               OR      photo_id > $pid
                )
          )
    ORDER BY hotel_id, idImageType, photo_id
    LIMIT 20
    

    并且拥有

    INDEX(hotel_id, idImageType, photo_id)
    

    这避免了orderid 的需要及其耗时的更新。

    一次分页一个hotel_id 会更简单。这行得通吗?

    编辑 2 -- 消除停机时间

    由于您会定期重新加载整个表,因此请在重新加载时执行此操作:

    1. CREATE TABLE New 建议更改索引。
    2. 将数据加载到New。 (请务必避免 51 分钟的超时;我不知道是什么原因造成的。)
    3. RENAME TABLE images TO old, New TO images;
    4. DROP TABLE old;

    这将避免因加载和架构更改而阻塞表。原子步骤 #3 将有一个非常短的块。

    计划在每次重新加载数据时执行此过程。

    另一个好处——在第 2 步之后,您可以测试新数据以查看它是否正常。

    【讨论】:

    • 问题是,这是一个海量的导入java程序,90天执行一次。它执行几个步骤没有问题,但是如果在一个步骤中,有一个 sql 挂起 51 分钟,则出现超时并且导入停止。导入时没有人使用数据库。结束导入后,我需要使用这个大表逐页阅读,但limit x after y 花费了太多时间,所以我在 orderid 上预设了一个顺序,而不是通过 orderid &gt;= y and orderid &lt; x 逐页阅读查询,这确保了一页的几毫秒时间。目前我按 (hotel_id,idImageType) 顺序分页。
    • 我认为删除 orderid 索引,进行更新并重新创建它将是一个很好的尝试。我尝试一下,然后发布结果。问候。
    • 我已经删除了 orderid 索引,更新结束重新创建索引:更新需要 45 分钟和 51 分钟,但删除索引并重新创建它需要太长时间。我将更改分页解决方案,坦克。
    • 跳过INDEX 更改;请参阅编辑 2
    • 51分钟不是MySql超时时间:是更新查询执行时间。这个时间对我的框架来说太长了(spring-batch + mybatis 3)。 spring-batch 步骤以未知状态退出。我正在尝试使 sql 更快。
    猜你喜欢
    • 2012-06-04
    • 2010-12-10
    • 1970-01-01
    • 2021-12-13
    • 1970-01-01
    • 2015-07-05
    • 2021-03-10
    • 2018-09-12
    • 1970-01-01
    相关资源
    最近更新 更多