6

我有一个相对较大的 4 深度关系数据设置,如下所示:

ClientApplication         has_many => ClientApplicationVersions
ClientApplicationVersions has_many => CloudLogs
CloudLogs                 has_many => Logs

client_applications: (可能有 1,000 条记录)
   - ...
   - account_id
   - public_key
   -deleted_at

client_application_versions: (可能有 10,000 条记录)
   - ...
   - client_application_id
   - public_key
   -deleted_at

cloud_logs: (可能有 1,000,000 条记录)
   - ...
   - client_application_version_id
   - public_key
   -deleted_at

logs: (可能有 1,000,000,000 条记录)
   - ...
   - cloud_log_id
   - public_key
   - time_stamp
   -deleted_at


我仍在开发中,所以结构和设置不是一成不变的,但我希望它设置好。使用 Rails 3.2.11 和 InnoDB MySQL。数据库当前填充了一小部分(与最终的数据库大小相比)数据集(logs只有 500,000 行)我有 4 个作用域查询,其中 3 个有问题,用于检索日志。

  1. 抓取第一页日志,按时间戳排序,限制为account_id, client_application.public_key, client_application_version.public_key(超过 100 秒)
  2. 抓取日志的第一页,按时间戳排序,限制为account_id, client_application.public_key(超过 100 秒)
  3. 抓取日志的第一页,按时间戳排序,限制为account_id(超过 100 秒)
  4. 获取日志的第一页,按时间戳排序(约 2 秒)

我正在使用 rails 范围来帮助进行这些调用:

  scope :account_id, proc {|account_id| joins(:client_application).where("client_applications.account_id = ?", account_id) }
  scope :client_application_key, proc {|client_application_key| joins(:client_application).where("client_applications.public_key = ?", client_application_key) }
  scope :client_application_version_key, proc {|client_application_version_key| joins(:client_application_version).where("client_application_versions.public_key = ?", client_application_version_key) }

  default_scope order('logs.timestamp DESC')

我在每个表上都有索引public_key。我在logs表上有几个索引,包括优化器喜欢使用的索引 ( index_logs_on_cloud_log_id),但是查询仍然需要 eons 才能运行。


以下是我如何调用该方法rails console

Log.account_id(1).client_application_key('p0kZudG0').client_application_version_key('0HgoJRyE').page(1)

...这是rails将其变成的内容:

SELECT `logs`.* FROM `logs` INNER JOIN `cloud_logs` ON `cloud_logs`.`id` = `logs`.`cloud_log_id` INNER JOIN `client_application_versions` ON `client_application_versions`.`id` = `cloud_logs`.`client_application_version_id` INNER JOIN `client_applications` ON `client_applications`.`id` = `client_application_versions`.`client_application_id` INNER JOIN `cloud_logs` `cloud_logs_logs_join` ON `cloud_logs_logs_join`.`id` = `logs`.`cloud_log_id` INNER JOIN `client_application_versions` `client_application_versions_logs` ON `client_application_versions_logs`.`id` = `cloud_logs_logs_join`.`client_application_version_id` WHERE (logs.deleted_at IS NULL) AND (client_applications.account_id = 1) AND (client_applications.public_key = 'p0kZudG0') AND (client_application_versions.public_key = '0HgoJRyE') ORDER BY logs.timestamp DESC LIMIT 100 OFFSET 0

...这是该查询的 EXPLAIN 语句。

+----+-------------+----------------------------------+--------+-------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------+---------+------------------------------------------------------------------------+------+----------------------------------------------+
| id | select_type | table                            | type   | possible_keys                                                                                                                                         | key                                               | key_len | ref                                                                    | rows | Extra                                        |
+----+-------------+----------------------------------+--------+-------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------+---------+------------------------------------------------------------------------+------+----------------------------------------------+
|  1 | SIMPLE      | client_application_versions      | ref    | PRIMARY,index_client_application_versions_on_client_application_id,index_client_application_versions_on_public_key                                    | index_client_application_versions_on_public_key   | 768     | const                                                                  |    1 | Using where; Using temporary; Using filesort |
|  1 | SIMPLE      | client_applications              | eq_ref | PRIMARY,index_client_applications_on_account_id,index_client_applications_on_public_key                                                               | PRIMARY                                           | 4       | cloudlog_production.client_application_versions.client_application_id  |    1 | Using where                                  |
|  1 | SIMPLE      | cloud_logs                       | ref    | PRIMARY,index_cloud_logs_on_client_application_version_id                                                                                             | index_cloud_logs_on_client_application_version_id | 5       | cloudlog_production.client_application_versions.id                     |  481 | Using where; Using index                     |
|  1 | SIMPLE      | cloud_logs_logs_join             | eq_ref | PRIMARY,index_cloud_logs_on_client_application_version_id                                                                                             | PRIMARY                                           | 4       | cloudlog_production.cloud_logs.id                                      |    1 |                                              |
|  1 | SIMPLE      | client_application_versions_logs | eq_ref | PRIMARY                                                                                                                                               | PRIMARY                                           | 4       | cloudlog_production.cloud_logs_logs_join.client_application_version_id |    1 | Using index                                  |
|  1 | SIMPLE      | logs                             | ref    | index_logs_on_cloud_log_id_and_deleted_at_and_timestamp,index_logs_on_cloud_log_id_and_deleted_at,index_logs_on_cloud_log_id,index_logs_on_deleted_at | index_logs_on_cloud_log_id                        | 5       | cloudlog_production.cloud_logs.id                                      |    4 | Using where                                  |
+----+-------------+----------------------------------+--------+-------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------+---------+------------------------------------------------------------------------+------+----------------------------------------------+

这个问题有 3 个部分:

  1. 我可以使用额外的索引来优化我的数据库,以帮助这些类型的连接相关的排序查询变得更高效吗?
  2. 我可以优化 rails 代码以帮助这种类型的find运行以更高效的方式运行吗?
  3. 我只是在接近这个范​​围内找到大型数据集的错误方法吗?




更新 1/24/12
正如 Geoff 和 J_MCCaffrey 在答案中所建议的,我已将查询分成 3 个不同的部分以尝试隔离问题。正如所料,这是处理最大表的问题。MYSQL 优化器通过使用不同的索引以不同方式处理此问题,但延迟仍然存在。这是这种方法的解释。

ClientApplication.find_by_account_id_and_public_key(1, 'p0kZudG0').versions.select{|cav| cav.public_key = '0HgoJRyE'}.first.logs.page(2)
  ClientApplication Load (165.9ms)  SELECT `client_applications`.* FROM `client_applications` WHERE `client_applications`.`account_id` = 1 AND `client_applications`.`public_key` = 'p0kZudG0' AND (client_applications.deleted_at IS NULL) ORDER BY client_applications.id LIMIT 1
  ClientApplicationVersion Load (105.1ms)  SELECT `client_application_versions`.* FROM `client_application_versions` WHERE `client_application_versions`.`client_application_id` = 3 AND (client_application_versions.deleted_at IS NULL) ORDER BY client_application_versions.created_at DESC, client_application_versions.id DESC
  Log Load (57295.0ms)  SELECT `logs`.* FROM `logs` INNER JOIN `cloud_logs` ON `logs`.`cloud_log_id` = `cloud_logs`.`id` WHERE `cloud_logs`.`client_application_version_id` = 49 AND (logs.deleted_at IS NULL) AND (cloud_logs.deleted_at IS NULL) ORDER BY logs.timestamp DESC, cloud_logs.received_at DESC LIMIT 100 OFFSET 100
  EXPLAIN (214.5ms)  EXPLAIN SELECT `logs`.* FROM `logs` INNER JOIN `cloud_logs` ON `logs`.`cloud_log_id` = `cloud_logs`.`id` WHERE `cloud_logs`.`client_application_version_id` = 49 AND (logs.deleted_at IS NULL) AND (cloud_logs.deleted_at IS NULL) ORDER BY logs.timestamp DESC, cloud_logs.received_at DESC LIMIT 100 OFFSET 100
EXPLAIN for: SELECT  `logs`.* FROM `logs` INNER JOIN `cloud_logs` ON `logs`.`cloud_log_id` = `cloud_logs`.`id` WHERE `cloud_logs`.`client_application_version_id` = 49 AND (logs.deleted_at IS NULL) AND (cloud_logs.deleted_at IS NULL) ORDER BY logs.timestamp DESC, cloud_logs.received_at DESC LIMIT 100 OFFSET 100
+----+-------------+------------+-------------+-------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------------------------------------------------------------+---------+-----------------------------------+------+-------------------------------------------------------------------------------------------------------------------------------------------------+
| id | select_type | table      | type        | possible_keys                                                                                                                                         | key                                                                              | key_len | ref                               | rows | Extra                                                                                                                                           |
+----+-------------+------------+-------------+-------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------------------------------------------------------------+---------+-----------------------------------+------+-------------------------------------------------------------------------------------------------------------------------------------------------+
|  1 | SIMPLE      | cloud_logs | index_merge | PRIMARY,index_cloud_logs_on_client_application_version_id,index_cloud_logs_on_deleted_at                                                              | index_cloud_logs_on_client_application_version_id,index_cloud_logs_on_deleted_at | 5,9     | NULL                              | 1874 | Using intersect(index_cloud_logs_on_client_application_version_id,index_cloud_logs_on_deleted_at); Using where; Using temporary; Using filesort |
|  1 | SIMPLE      | logs       | ref         | index_logs_on_cloud_log_id_and_deleted_at_and_timestamp,index_logs_on_cloud_log_id_and_deleted_at,index_logs_on_cloud_log_id,index_logs_on_deleted_at | index_logs_on_cloud_log_id                                                       | 5       | cloudlog_production.cloud_logs.id |    4 | Using where                                                                                                                                     |
+----+-------------+------------+-------------+-------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------------------------------------------------------------+---------+-----------------------------------+------+-------------------------------------------------------------------------------------------------------------------------------------------------+




2012 年 1 月 25 日更新
以下是所有相关表的索引:

CLIENT_APPLICATIONS:
  PRIMARY KEY  (`id`),
  UNIQUE KEY `index_client_applications_on_key` (`key`),
  KEY `index_client_applications_on_account_id` (`account_id`),
  KEY `index_client_applications_on_deleted_at` (`deleted_at`),
  KEY `index_client_applications_on_public_key` (`public_key`)

CLIENT_APPLICATION_VERSIONS:
  PRIMARY KEY  (`id`),
  KEY `index_client_application_versions_on_client_application_id` (`client_application_id`),
  KEY `index_client_application_versions_on_deleted_at` (`deleted_at`),
  KEY `index_client_application_versions_on_public_key` (`public_key`)

CLOUD_LOGS:
  PRIMARY KEY  (`id`),
  KEY `index_cloud_logs_on_api_client_version_id` (`api_client_version_id`),
  KEY `index_cloud_logs_on_client_application_version_id` (`client_application_version_id`),
  KEY `index_cloud_logs_on_deleted_at` (`deleted_at`),
  KEY `index_cloud_logs_on_device_id` (`device_id`),
  KEY `index_cloud_logs_on_public_key` (`public_key`),
  KEY `index_cloud_logs_on_received_at` (`received_at`)

LOGS:
  PRIMARY KEY  (`id`),
  KEY `index_logs_on_class_name` (`class_name`),
  KEY `index_logs_on_cloud_log_id_and_deleted_at_and_timestamp` (`cloud_log_id`,`deleted_at`,`timestamp`),
  KEY `index_logs_on_cloud_log_id_and_deleted_at` (`cloud_log_id`,`deleted_at`),
  KEY `index_logs_on_cloud_log_id` (`cloud_log_id`),
  KEY `index_logs_on_deleted_at` (`deleted_at`),
  KEY `index_logs_on_file_name` (`file_name`),
  KEY `index_logs_on_method_name` (`method_name`),
  KEY `index_logs_on_public_key` (`public_key`),
  KEY `index_logs_on_timestamp` USING BTREE (`timestamp`)
4

7 回答 7

1

以更好的结构显示它,查询看起来像这样(已经重新排列)

SELECT
  `logs`.*
FROM
  `logs` as l
  INNER JOIN `cloud_logs` as cl1
    ON
      cl1.id = l.cloud_log_id
  INNER JOIN `cloud_logs` as cl2
    ON
      cl2.id = l.cloud_log_id
  INNER JOIN `client_application_versions` as cav1
    ON
      cav1.id = cl1.client_application_version_id
  INNER JOIN `client_application_versions` as cav2
    ON
      cav2.id = cl2.client_application_version_id
  INNER JOIN `client_applications` as ca
    ON
      ca.id = cav1.client_application_id
WHERE
  (l.deleted_at IS NULL)
    AND
  (ca.account_id = 1)
    AND
  (ca.public_key = 'p0kZudG0')
    AND
  (cav.public_key = '0HgoJRyE')
ORDER BY
  logs.timestamp DESC
LIMIT
  0, 100

在查看 cav1/cl1 和 cav2/cl2 时,可以看到,从未使用过 cav2 和 cl2。除了 ON 语句之外,没有对其应用过滤器。

因此 cav1 链接到正确的帐户,cav2 不链接到任何帐户,而是包含所有匹配的帐户。这对于查询的结果没有问题,但对于连接缓冲区的大小。

删除连接(以及其中的一部分)会产生:

SELECT
  `logs`.*
FROM
  `logs` as l
  INNER JOIN `cloud_logs` as cl1
    ON
      cl1.id = l.cloud_log_id
--  INNER JOIN `cloud_logs` as cl2
--    ON
--      cl2.id = l.cloud_log_id
  INNER JOIN `client_application_versions` as cav1 use index for join (`index_cavs_on_client_application_id_and_public_key`)
    ON
      cav1.id = cl1.client_application_version_id
        AND
      cav1.public_key = '0HgoJRyE'

--  INNER JOIN `client_application_versions` as cav2
--    ON
--      cav2.id = cl2.client_application_version_id
  INNER JOIN `client_applications` as ca
    ON
      ca.id = cav1.client_application_id
WHERE
  (l.deleted_at IS NULL)
    AND
  (ca.account_id = 1)
    AND
  (ca.public_key = 'p0kZudG0')
ORDER BY
  logs.timestamp DESC
LIMIT
  0, 100

这个应该更快。

将其打包到控制台中可用的东西中(假设正确的表关系和 meta_where):

Log.where(:deleted_at.ne => nil).order("logs.timestamp desc").joins(:cloud_logs) & \
CloudLog.joins(:client_application_versions) & \
ClientApplicationVersion.where(:public_key => '0HgoJRyE').joins(:client_applications) & \
ClientApplication.where(:public_key => 'p0kZudG0', :account_id => 1)

由于我无法在此处重现此内容,因此您可能必须自己尝试(或在末尾添加 to_sql),并为上面的缩短查询添加说明。

结果可能很有趣。

更新:看到结果和定义后(下面的评论):

尝试添加密钥:

alter table client_application_versions add key (`client_application_id`, `public_key`);

这应该可以防止文件排序并加快速度。

编辑:更新查询以提示 mysql 关于密钥。

于 2013-01-25T02:27:36.340 回答
0

不幸的是,我的 Rails 优化经验都是 PostgreSQL 的,所以大部分可能都不适用。不过,我确实有一些可能适用的建议:

尝试在您的范围内使用joins而不是-用于触发急切加载 - 您看到的某些减速完全有可能是加载了不需要的模型。即使不是,使用instead 应该会产生一个更具可读性的查询 - 就是将所有列别名为“t2_r8”,依此类推。includesincludesjoinsincludes

此外,您需要确保所有可能被过滤的列都被索引 - 一般来说,_id以这种方式结尾的列可能会被引用,并且可能应该被索引,以及您专门过滤的任何列在范围内(如client_application_version_key

于 2013-01-19T22:33:23.977 回答
0

我正在为我自己的问题写一个可能的解决方案,希望能有更好的答案。目前,数据库是完全建立的,并且是按书关系建立的。

ClientApplication         has_many => ClientApplicationVersions
ClientApplicationVersions has_many => CloudLogs
CloudLogs                 has_many => Logs

这意味着当我需要查找属于客户端应用程序的日志时,我必须进行 3 次额外的连接才能获得它。通过向 Logs 表引入一些foreign_key非规范化,我可以跳过所有连接:

ClientApplication         has_many => ClientApplicationVersions
ClientApplication         has_many => Logs
ClientApplicationVersions has_many => CloudLogs
ClientApplicationVersions has_many => Logs
CloudLogs                 has_many => Logs

最终结果是我的 Logs 表中会有一些额外的列:client_application_keyclient_application_version_keycloud_log_key

尽管我冒着数据不一致的风险,但我可以避免这里的 3 个连接有助于降低查询的性能。有人请告诉我这件事。

于 2013-01-20T16:13:13.117 回答
0

试图回答你的每一个问题:

  1. 当然!您正在搜索的任何内容都应该被编入索引。如果没有索引,则必须进行全表扫描。references如果您使用 in 中的函数,在您进行初始迁移时将创建的关联 ID 之上create_table,您至少要搜索以下内容:

    • 日志.时间戳
    • client_application_versions.public_key
    • client_applications.public_key
    • logs.deleted_at

    这些可能都应该被索引。当然,如果您在定义关联外键时没有使用references,那么也添加它们。当然,与指数有一个权衡。它们就像读取的魔法一样,但它们可能会显着减慢您的写入速度。它们减慢或加快速度的程度也可能在很大程度上取决于数据库。

  2. 我不这么认为。你的 Rails 代码对我来说看起来很合适。我唯一的评论是,这scope实际上只是定义函数的一种简写方式。我认为如果你直接定义函数会更容易阅读:

    self.account_id(account_id)
      joins(:client_application).where("client_applications.account_id = ?", account_id)
    end
    
  3. 也许!不幸的是,这不是一个容易回答的问题,因为它确实取决于数据的样子。您真的需要同一个数据库表中的数十亿条日志吗?是否有某种方法可以自然地将数据分解为具有相同架构的不同表?您可能还想研究数据库分片。

我希望这会有所帮助。

编辑:

为什么要朝着最痛苦的方向进行查询?您是否尝试过从以下方面扭转它:

Log.account_id(1).client_application_key('p0kZudG0').client_application_version_key('0HgoJRyE').page(1)

对于这样的事情:

ClientApplication.find_all_by_account_id(1).where(public_key: 'p0kZudG0').joins(:client_application_version).where("client_application_versions.public_key=?",'0HgoJRyE').logs.page(1)

您应该定义一些范围以使其更具可读性,但希望您能理解。

于 2013-01-21T22:33:44.350 回答
0

当我遇到这样的查询性能问题时,我会查看 Rails 正在做什么,并找出是否有更好的方法来获得我想要的。大多数时候 Rails 查询会很好,但有时你会意识到你可以得到你需要的更快/更清洁的方式。

您可能能够在 2 个查询中获得您想要的内容,但我会首先分解连接并查看如果您输入从连接中获得的数据,查询的执行情况。

您是否在没有限制和偏移的情况下测试了结果?我想排除排序和限制部分,并观察性能。我以前看到过限制和偏移的大问题,并且有一些方法可以调整 mysql 如何处理内存中的排序,而不是像你目前正在做的那样使用临时表和文件排序。

编辑

您可以先查询 id,然后根据 id 查询所有列。

从内部连接中选择logs.id 。= 。在哪里。= 49 AND (logs.deleted_at IS NULL) AND (cloud_logs.deleted_at IS NULL) ORDER BY logs.timestamp DESC 限制 100logscloud_logslogscloud_log_idcloud_logsidcloud_logsclient_application_version_id

这个查询快吗?(它应该能够在不扫描表的情况下从索引中获取 id)更具侵入性的更改可能是在数据库级别对数据进行分区,但我认为现在建议这样做还为时过早。

于 2013-01-23T22:48:15.053 回答
0

您的索引定义不正确index_logs_on_cloud_log_id_and_deleted_at_and_timestamp

您的查询花费太长时间的部分是order by子句,您正在排序,timestamp但它timestamp是索引的最后一个键。在 MySQL 中,索引中后面的键将不会用于优化,order by除非前面的键是where子句中的常量。请参阅http://dev.mysql.com/doc/refman/5.0/en/order-by-optimization.html

对于初学者,只需创建一个索引timestamp并查看是否可以加快查询速度。

于 2013-01-25T05:17:19.030 回答
0

嗯,这有点难,因为为了获得性能,你必须牺牲可读性,或者反过来。所以,回答你的问题:

  1. 索引是一个想法,但它的成功可能因表大小以及查询运行的频率和键组合而异。但在我看来,您查询的是同一件事,只是订购方式不同。那么......为什么不使用数据库视图呢?他们在 Rails 中的实现很糟糕,但它是可用的:https ://github.com/eladmeidar/PlainViews
  2. (忘记你是否同意我的观点建议)是的,你可以。不要使用范围。ARel 已经使它们实际上已经过时了,在你的例子中绝对是这样,因为你可以只使用 ARel 来进行描述。还有一件事:不要用 ARel 编写 SQL。你会错过很多好东西,比如重命名表以适应连接和东西,这可能会弄乱你的索引。更像这样的东西:

    YourObject.joins(:client_application).
               where(ClientApplication.arel_table[:public_key].eq(client_application_key))
    
  3. 取决于你要在哪里使用它。如果查询是您的应用程序功能的核心,那么您应该以其他方式进行更多调查,以提高其性能。例如,DB 提供了很多 Web 框架(尤其是 Rails)不提倡的特性,比如上面提到的视图或存储过程。如何利用这一点并保持代码可读性是我们开发人员每天面临的挑战。

但是我还有一个问题:为什么不使用 MongoDB?http://nosql.mypopescu.com/post/1016320617/mongodb-is-web-scale

于 2013-01-28T15:47:41.300 回答