【问题标题】:Normalizing/optimizing structure of large mysql table大型mysql表的规范化/优化结构
【发布时间】:2011-11-13 00:41:53
【问题描述】:

我有一个网站,里面有很多用户,还有很多“节点”(内容)。每个节点都可以下载,除了相关的特定节点 ID 之外,每个下载都有一个与之关联的“许可证”(因此用户可以下载节点 5 用于“商业用途”或“个人用途”等),如以及每个许可证的价格。

我的目标是以这样一种方式跟踪下载,以便我能够:

  • 获取给定节点 ID 和许可证 ID 在给定时间段内的下载次数(上个月节点 5 出于“商业用途”下载了多少次?)。
  • 获取给定节点 ID 和许可证 ID 的总下载次数。
  • 获取给定 node_id 的下载次数,无论许可证如何(“商业用途”和“个人用途”的所有下载相结合)。
  • 获取满足给定价格标准(即价格 = 0 或价格 > 0)的给定用户已下载的节点 ID(和相应的许可证 ID)。

如果优化无关紧要,存储的数据微不足道,但我的问题是对可能很容易增长到数百万行的表进行规范化/优化。具体来说,假设:

  • 下载量千万级。
  • 节点数达数十万。
  • 用户数以万计。

我对任何“真正的”mysql 工作都很陌生,所以我感谢你的帮助,并指出我在哪里很愚蠢。到目前为止,这是我所得到的:

all_downloads 表

   +-------------+---------+------------+---------+-----------+-------+
   | download_id | node_id | license_id | user_id | timestamp | price |
   +-------------+---------+------------+---------+-----------+-------+

download_id 是该表的唯一键。这个表是个问题,因为它可能有数千万行。

downloads_counted 表

不是通过查询 all_downloads 表来计算给定节点和许可证的下载总数,而是在 cron 运行期间对下载进行计数,并将这些数字单独存储在 downloads_counted 表中:

   +---------------------------------------------------------------------------+
   | node_id | license_id | downloads_total | downloads_month | downloads_week |  
   +---------------------------------------------------------------------------+

许可证 id 情况是新的(以前只有一个许可证,因此数据库中没有跟踪许可证),所以我现在只是想弄清楚如何使用它。过去,node_id 是该表的唯一键。我假设我现在应该做的是将 node_id 和 license_id 组合成一个唯一的主键。还是将 node_id 作为该表的唯一键,并获取给定 node_id 的所有行,然后在 php 中解析结果(分离或组合每个特定许可证的下载)是否也一样好?拥有一个没有唯一键的表是否符合最佳实践?

在任何情况下,我认为这个表大部分都可以,因为它不应该增长到超过 1 或 200 万行。

为给定用户返回下载的问题

这是我需要帮助的主要领域。我考虑过将 user_id 设置为 all_downloads 表中的键,并简单地查询包含给定 user_id 的所有行。但我担心从长远来看查询这张表,因为它从一开始就非常大,很容易增长到几千万行。

我考虑过创建一个如下所示的 user_downloads 表:

   +---------------------+
   | user_id | downloads | 
   +---------------------+

其中下载将是 node_ids 的序列化数组以及相关的许可证 ID 和价格,如下所示(5 是 node_id,将是顶级 node_ids 数组中的索引):

downloads = array('5' = array(license = array('personal', 'commercial'), price = 25))

我意识到将数据数组存储在单个单元格中被认为是一种不好的做法,而且我不确定它是否会提高性能,因为对于给定的用户,下载数组很容易增长到数千个。但是,我不确定如何创建另一个表结构,它比我的 all_downloads 表更有效地获取给定用户的下载。

非常感谢任何和所有帮助!

======================================

Bill Karwin 回答的后续问题:

  • 不幸的是,时间戳将是一个存储在 int(11),而不是日期时间(符合 Drupal 标准)。我 假设这并没有真正改变优化 立场?

  • node_id/license_id/user_id(您对集群主键的想法)是 不保证是唯一的,因为用户可以根据需要多次下载同一许可证下的同一节点。这个 是我为每一行拥有唯一的 download_id 的主要原因...... 有一个特殊的原因有一个 download_id 会损害性能吗?或者将主键设为download_id/node_id/license_id/user_id的集群是否可以接受?还是将 download_id 作为复合键的第一部分会失去它的用处?

  • 您认为拥有 downloads_counted 表是否仍然有意义,或者这会被认为是多余的?我的想法是它仍然有助于提高性能,因为下载计数(总下载量、本周、本月等)将在网站上经常出现非常,并且 downloads_counted 表会比 all_downloads 表少一到两个数量级的行。

我对 downloads_counted 表的想法:

CREATE TABLE downloads_counted (   
 node_id          INT UNSIGNED NOT NULL,   
 license_id       INT UNSIGNED NOT NULL, 
 downloads_total  INT UNSIGNED NOT NULL,  
 downloads_month  INT UNSIGNED NOT NULL,   
 downloads_week   INT UNSIGNED NOT NULL,     
 downloads_day    INT UNSIGNED NOT NULL,  
 PRIMARY KEY (node_id, license_id), 
 KEY (node_id)
) ENGINE=InnoDB;

node_id 上的辅助键用于获取给定 node_id 的所有许可证的所有下载...但是,如果 node_id 已经是复合主键的第一部分,这个键是否多余?

【问题讨论】:

    标签: mysql database database-design


    【解决方案1】:

    这是我设计表格的方式:

    CREATE TABLE all_downloads (
      node_id    INT UNSIGNED NOT NULL,
      license_id INT UNSIGNED NOT NULL,
      user_id    INT UNSIGNED NOT NULL,
      timestamp  DATETIME NOT NULL,
      price      NUMERIC (9,2),
      PRIMARY KEY (node_id,license_id,user_id),
      KEY (price)
    ) ENGINE=InnoDB;
    

    请注意,我省略了 download_id。

    现在您可以运行所需的查询:

    • 获取给定节点 ID 和许可证 ID 在给定时间段内的下载次数(上个月节点 5 出于“商业用途”下载了多少次?)。

      SELECT COUNT(*) FROM all_downloads WHERE (node_id,license_id) = (123,456) 
      AND timestamp > NOW() - INTERVAL 30 DAY
      

      这应该很好地利用聚集主索引,减少检查的行集,直到时间戳比较只适用于一个小子集。

    • 获取给定节点 ID 和许可证 ID 的总下载次数。

      SELECT COUNT(*) FROM all_downloads WHERE (node_id,license_id) = (123,456);
      

      与上面一样,这利用了聚集主索引。通过索引扫描完成计数。

    • 获取给定 node_id 的下载次数,无论许可证如何(“商业用途”和“个人用途”的所有下载相结合)。

      SELECT COUNT(*) FROM all_downloads WHERE (node_id) = (123);
      

      同上。

    • 获取给定用户已下载且满足给定价格标准(即价格 = 0 或价格 > 0)的节点 ID(和相应的许可证 ID)。

      SELECT node_id, license_id FROM all_downloads WHERE price = 0 AND user_id = 789;
      

      这会减少使用price 上的二级索引检查的行数。然后,您可以利用 InnoDB 中的二级索引隐含包含主键列的事实,因此您甚至不需要读取基础数据。这称为覆盖索引或仅索引查询。

    至于你的其他问题:


    timestamp ... 从优化的角度来看并没有真正改变任何东西?

    我更喜欢 datetime 而不是 timestamp 只是因为 datetime 包含时区信息,而 timestamp 不包含。您始终可以使用 UNIX_TIMESTAMP() 函数将查询结果中的日期时间转换为 UNIX 时间戳整数。

    将主键设为download_id/node_id/license_id/user_id的集群是否可以接受?还是将 download_id 作为复合键的第一部分会失去它的用处?

    聚集键的好处是行按索引的顺序存储。因此,如果您经常根据 node_id 进行查询,那么将其放在复合聚簇索引的首位会有性能优势。 IE。如果您对给定 node_id 的行集感兴趣,将它们存储在一起是一个好处,因为您以这种方式定义了聚集索引。

    您认为拥有一个 downloads_counted 表是否仍然有意义,或者这会被认为是多余的?

    当然,将汇总结果存储在表格中是一种常用方法,可以减少频繁计算经常需要的总数的工作量。但是要明智地这样做,因为要使这些总数与真实数据保持同步需要一些工作。如果您需要经常阅读预先计算的总数,并且每次更新时都要多次阅读,那么好处会更大。确保您将汇总的总数视为不如实际下载数据的权威,并制定计划在它们不同步时重新生成总数。

    有些人还将这些聚合放入 memcached 键而不是表中,以便更快地查找。如果 memcached 中的 volatile 数据由于某种原因丢失,您可以从下载数据中重新填充。

     PRIMARY KEY (node_id, license_id), 
     KEY (node_id)
    ) ENGINE=InnoDB;
    

    如果 node_id 已经是复合主键的第一部分,那么这个键是多余的吗?

    是的。 MySQL 允许您创建冗余索引,这是冗余索引的一个示例。任何可以使用 node_id 上的辅助键的查询都可以很容易地使用主键。事实上,在这种情况下,优化器将从不使用辅助键,因为它会更喜欢主键的聚集索引。

    您可以使用pt-duplicate-key-checker 分析数据库中的冗余索引。

    【讨论】:

    • 在不衡量性能的情况下假设存在性能问题也不是一个好习惯。
    • 感谢您非常有帮助和启发性的回复比尔!我觉得我从你的回复中学到了更多关于 sql 的知识,而不是从我最近的谷歌搜索中学到的。无论如何,如果您有时间并且很乐意解决这些问题,我已经在我的原始帖子中添加了几个快速跟进问题。
    • @Catcall 我同意。我处理这个下载表的目标不是假设性能是一个问题,而是简单地从一开始就在似乎可能是性能问题的情况下创建我能做到的最佳设计......只是环顾四周我'我们发现许多情况下,人们在查询具有数千万行的表时遇到性能问题。反正我的sql知识很差,只想打好基础。我在衡量绩效之前跳到不良实践的想法当然是一个糟糕的想法,我很感激你打电话给我。
    • @JordanMagnuson:最佳实践是只执行插入并捕获错误——数据库往返一次。检查再插入需要两次往返,而主键已经存在的情况还是要写代码。
    • 你也可以使用INSERT... ON DUPLICATE KEY UPDATE download_count=download_count+1
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-03-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-21
    • 2018-06-06
    相关资源
    最近更新 更多