【问题标题】:MySQL performance: multiple tables vs. index on single table and partitionsMySQL 性能:多个表与单个表和分区上的索引
【发布时间】:2013-05-19 06:37:25
【问题描述】:

我想知道什么更高效,性能更快:
在一个大表或多个没有索引的小表上有索引?

由于这是一个非常抽象的问题,让我把它变得更实用:
我有一张关于用户统计信息的表格(20,000 个用户和大约 3000 万行)。该表大约有 10 列,包括 user_idactionstimestamps 等。
最常见的应用是:通过user_id 插入数据和通过user_id 检索数据(SELECT 语句从不包含多个user_id's)。

到目前为止,我在user_id 上有一个INDEX,查询看起来像这样

SELECT * FROM statistics WHERE user_id = 1

现在,随着行数越来越多,表格变得越来越慢。 INSERT 语句变慢了,因为 INDEX 越来越大; SELECT 语句变慢了,嗯,因为要搜索的行更多。

现在我想知道为什么不为每个用户创建一个统计表,而是将查询语法改为这样:

SELECT * FROM statistics_1

1 显然代表user_id
这样一来,就不需要INDEX,而且每个表中的数据也少得多,所以INSERTSELECT语句应该会快得多。

现在我的问题又来了:
在处理这么多表(在我的情况下是 20,000 个)而不是使用一个带有 INDEX 的表时,是否有任何现实世界的缺点?
我的方法真的会加快速度吗?或者查找表格最终会减慢速度而不是一切?

【问题讨论】:

  • 这是个糟糕的主意。你确定你的索引设置正确吗?统计信息的主键(即聚集索引)应包含user_id。
  • 是的,索引设置正确,查询也不慢。我只是想知道这种不同的架构是否会提高性能。
  • "没有索引的表" -- 哎呀,这不是首发。

标签: mysql performance indexing


【解决方案1】:

具体例子:

我有一张关于用户统计信息的表(20,000 个用户和大约 3000 万行)。该表有大约 10 列,包括 user_id、操作、时间戳等。 最常见的应用是:通过 user_id 插入数据并通过 user_id 检索数据(SELECT 语句从不包含多个 user_id)。

这样做:

id INT UNSIGNED NOT NULL AUTO_INCREMENT,
 ...
PRIMARY KEY(user_id, id),
INDEX(id)

在PK 的开始 处有user_id 为您提供“参考位置”。也就是说,一个用户的所有行都聚集在一起,从而最大限度地减少 I/O。

PK 的end 上的id 是因为 PK 必须是唯一的。

长相怪异的INDEX(id)是为了让AUTO_INCREMENT开心。

抽象问题:

  • 永远不要有多个相同的表。
  • 仅当满足http://mysql.rjweb.org/doc.php/partitionmaint 中列出的用例之一时才使用PARTITIONing
  • PARTITIONed 表需要一组与非分区等效表不同的索引。
  • 在大多数情况下,单个非分区表是最佳选择。
  • 使用查询来设计索引。

【讨论】:

    【解决方案2】:

    创建 20,000 个表是个坏主意。不久之后,您将需要 40,000 张桌子,甚至更多。

    我在我的书 SQL Antipatterns 中称这种综合症为 Metadata Tribbles。每次您计划创建“每个 X 的表”或“每个 X 的列”时都会发生这种情况。

    当您有数以万计的表时,这确实会导致真正的性能问题。每个表都需要 MySQL 维护内部数据结构、文件描述符、数据字典等。

    还有实际的操作后果。您真的要创建一个要求您在每次新用户注册时都创建一个新表的系统吗?

    相反,我建议您使用MySQL Partitioning

    这是一个对表进行分区的示例:

    CREATE TABLE statistics (
      id INT AUTO_INCREMENT NOT NULL,
      user_id INT NOT NULL,
      PRIMARY KEY (id, user_id)
    ) PARTITION BY HASH(user_id) PARTITIONS 101;
    

    这为您提供了定义一个逻辑表的好处,同时还将表划分为许多物理表,以便在您查询分区键的特定值时更快地访问。

    例如,当您像示例一样运行查询时,MySQL 仅访问包含特定 user_id 的正确分区:

    mysql> EXPLAIN PARTITIONS SELECT * FROM statistics WHERE user_id = 1\G
    *************************** 1. row ***************************
               id: 1
      select_type: SIMPLE
            table: statistics
       partitions: p1    <--- this shows it touches only one partition 
             type: index
    possible_keys: NULL
              key: PRIMARY
          key_len: 8
              ref: NULL
             rows: 2
            Extra: Using where; Using index
    

    HASH 分区方法意味着通过整数分区键的模数将行放置在分区中。这确实意味着许多 user_id 映射到同一个分区,但每个分区平均只有 1/N 的行数(其中 N 是分区数)。并且您使用恒定数量的分区定义表,因此您不必每次获得新用户时都对其进行扩展。

    您可以选择最多 1024 个(或 MySQL 5.6 中为 8192 个)的任意数量的分区,但有些人报告说当分区数达到这么高时会出现性能问题。

    建议使用素数分区。如果您的 user_id 值遵循某种模式(例如仅使用偶数),则使用质数分区有助于更均匀地分布数据。


    在评论中回答您的问题:

    如何确定合理数量的分区?

    对于 HASH 分区,如果您使用 101 个分区,就像我在上面的示例中显示的那样,那么任何给定的分区平均大约有 1% 的行。你说你的统计表有 3000 万行,所以如果你使用这个分区,每个分区只有 300k 行。这对 MySQL 来说更容易阅读。您也可以(并且应该)使用索引——每个分区都有自己的索引,它只有整个未分区表上的索引的 1%。

    那么如何确定合理的分区数量的答案是:你的整个表有多大,你希望分区平均有多大?

    分区的数量不应该随着时间的推移而增长吗?如果是这样:我该如何实现自动化?

    如果您使用 HASH 分区,则不一定需要增加分区数。最终你可能总共有 300 亿行,但我发现当你的数据量增长几个数量级时,无论如何都需要一个新的架构。如果您的数据增长到那么大,您可能需要在多台服务器上分片以及分区到多个表中。

    也就是说,您可以使用 ALTER TABLE 重新分区表:

    ALTER TABLE statistics PARTITION BY HASH(user_id) PARTITIONS 401;
    

    这必须重组表(就像大多数 ALTER TABLE 更改一样),所以预计需要一段时间。

    您可能想要监控分区中数据和索引的大小:

    SELECT table_schema, table_name, table_rows, data_length, index_length
    FROM INFORMATION_SCHEMA.PARTITIONS
    WHERE partition_method IS NOT NULL;
    

    与任何表一样,您希望活动索引的总大小适合您的缓冲池,因为如果 MySQL 在 SELECT 查询期间必须将部分索引交换进出缓冲池,则性能会受到影响。

    如果您使用 RANGE 或 LIST 分区,则添加、删除、合并和拆分分区更为常见。见http://dev.mysql.com/doc/refman/5.6/en/partitioning-management-range-list.html

    我鼓励您阅读manual section on partitioning,并查看这个精彩的演示文稿:Boost Performance With MySQL 5.1 Partitions

    【讨论】:

    • 感谢您的有用回答。但是:如何确定合理数量的分区?并且:分区的数量不应该随着时间的推移而增长吗?如果是这样:我该如何实现自动化?
    • +1 表示分区的素数技巧:顺便说一句,如果您更改表并添加更多分区,mysql 是否知道自动重新分配数据?
    • @Stephan,是的,ALTER TABLE 根据新的分区方案重建整个表。
    • @BillKarwin 很好的答案 - 感谢您回答其他问题!
    • 我对你的例子提出了质疑。未分区的等效项将具有INDEX(user_id),并且对user_id 的点查询将运行得更快或更快。
    【解决方案3】:

    Bill Karwins 的回答没有什么可补充的。但一个提示是:检查用户的所有数据是否始终完整详细。

    如果您想提供使用情况统计信息或访问次数或其他内容,通常不会获得单个操作和秒数的粒度,例如从今天的角度来看 2009 年。因此,您可以构建聚合表和存档表(当然不是引擎存档),以获取有关操作库的最新数据以及对旧操作的概述。

    我认为旧的行为不会改变。

    例如,您仍然可以从归档表中带有 week_id 的聚合中详细了解。

    【讨论】:

      【解决方案4】:

      这可能取决于您计划经常进行的查询类型,确定的最佳方法是实现两者的原型并进行一些性能测试。

      话虽如此,我希望带有索引的单个(大)表总体上会做得更好,因为大多数 DBMS 系统都经过大量优化,可以处理在大表中查找和插入数据的确切情况。如果您尝试创建许多小表以希望提高性能,那么您有点在与优化器作斗争(这通常会更好)。

      另外,请记住,一张桌子将来可能更实用。如果您想获得所有用户的一些汇总统计信息怎么办?拥有 20 000 个表会使执行起来非常困难且效率低下。这些模式的灵活性也值得考虑。如果您像这样对表进行分区,您可能会将自己设计成未来的角落。

      【讨论】:

        【解决方案5】:

        除了每个用户从 1 个表变为 1 个表之外,您可以使用分区来达到中间某个位置的多个表/表大小比率。

        您还可以保留有关用户的统计信息,以尝试将“活跃”用户移动到 1 个表中,以减少随着时间的推移您必须访问的表数量。

        最重要的是,您可以做很多事情,但主要是您必须构建原型和测试,并评估您所做的各种更改对性能的影响。

        【讨论】:

          猜你喜欢
          • 2012-02-29
          • 1970-01-01
          • 1970-01-01
          • 2017-02-21
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多