【问题标题】:SQL MAX on primary key, is filter condition unncessary if it is already indexed?主键上的 SQL MAX,如果它已经被索引,过滤条件是否不必要?
【发布时间】:2021-12-19 21:55:48
【问题描述】:
select MAX(id) from studenthistory 
where class_id = 1 
   and date(created_at) = '2021-11-05' 
   and time(created_at) > TIME('04:00:00') 
group by student_id

composite indexes = ("class_id", "student_id", "created_at")

id 是主键。

date(created_at) = '2021-11-05' and time(created_at) > TIME('04:00:00') 过滤条件对于 Max 函数是否不必要,因为 studenthistory 已在 class_id 和 student_id 上建立索引?

我添加日期时间过滤器的唯一原因是该表会随着时间的推移变得越来越大。 (历史数据)我想减少查询必须搜索的行数。

但是对于 Max 函数的情况 - 我相信 MAX 会简单地获取最后一个值而不检查整行,如果它被索引的话。

所以我可以安全地删除日期时间过滤器并将其变成

select MAX(id) from studenthistory 
where class_id = 1 
group by student_id

并且有同样的表现? (或者更好,因为它不需要进一步过滤?)

检查查询计划似乎性能相似,但表的大小目前相当小..

第一:

| -> Group aggregate: max(id)  (cost=1466.30 rows=7254) (actual time=2.555..5.766 rows=3 loops=1)
    -> Filter: ((cast(studenthistory.created_at as date) = '2021-11-05') and (cast(riderlocation.created_at as time(6)) > <cache>(cast('04:00:00' as time))))  (cost=740.90 rows=7254) (actual time=0.762..5.384 rows=5349 loops=1)
        -> Index lookup on studenthistory using idx_studenthistory_class_id_931474 (class_id=1)  (cost=740.90 rows=7254) (actual time=0.029..3.589 rows=14638 loops=1)
 |

1 row in set (0.00 sec)

第二:

| -> Group aggregate: max(studenthistory.id)  (cost=1475.40 rows=7299) (actual time=0.545..5.271 rows=10 loops=1)
    -> Index lookup on studenthistory using idx_studenthistory_class_id_931474 (class_id=1)  (cost=745.50 rows=7299) (actual time=0.026..4.164 rows=14729 loops=1)
 |
1 row in set (0.01 sec)

在此先感谢


更新:应用@rick james 的建议:

将索引更改为 (class_id, student_id, id)。

FLUSH STATUS;
explain FORMAT=JSON SELECT MAX(`id`) `0` FROM `studenthistory`
    WHERE `class_id`=1 AND `created_at`>='2021-11-05T18:25:50.544850+00:00'
    GROUP BY `student_id`;


| {
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "940.10"
    },
    "grouping_operation": {
      "using_filesort": false,
      "table": {
        "table_name": "studenthistory",
        "access_type": "ref",
        "possible_keys": [
          "fk_studenthist_student_e25b0310",
          "idx_studenthistory_class_id_931474"
        ],
        "key": "idx_studenthistory_class_id_931474",
        "used_key_parts": [
          "class_id"
        ],
        "key_length": "4",
        "ref": [
          "const"
        ],
        "rows_examined_per_scan": 8381,
        "rows_produced_per_join": 2793,
        "filtered": "33.33",
        "cost_info": {
          "read_cost": "102.00",
          "eval_cost": "279.34",
          "prefix_cost": "940.10",
          "data_read_per_join": "130K"
        },
        "used_columns": [
          "id",
          "created_at",
          "student_id",
          "class_id"
        ],
        "attached_condition": "(`test-table`.`studenthistory`.`created_at` >= TIMESTAMP'2021-11-05 18:25:50.54485')"
      }
    }
  }
} |

即只有 class_id 用作索引,(因为 created_at 不再在索引中。由于过滤器,rows_produced_per_join 较低:2793,

没有日期时间过滤器:

FLUSH STATUS;
mysql> explain FORMAT=JSON SELECT MAX(`id`) `0` FROM `studenthistory`
    WHERE `class_id`=1  GROUP BY `student_id`;


| {
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "854.75"
    },
    "grouping_operation": {
      "using_filesort": false,
      "table": {
        "table_name": "studenthistory",
        "access_type": "ref",
        "possible_keys": [
          "fk_studenthistory_student_e25b0310",
          "idx_studenthistory_class_id_931474"
        ],
        "key": "idx_studenthistory_class_id_931474",
        "used_key_parts": [
          "class_id"
        ],
        "key_length": "4",
        "ref": [
          "const"
        ],
        "rows_examined_per_scan": 8381,
        "rows_produced_per_join": 8381,
        "filtered": "100.00",
        "using_index": true,
        "cost_info": {
          "read_cost": "16.65",
          "eval_cost": "838.10",
          "prefix_cost": "854.75",
          "data_read_per_join": "392K"
        },
        "used_columns": [
          "id",
          "student_id",
          "class_id"
        ]
      }
    }
  }
} |

在所有 3 个索引(“class_id”、“student_id”、“id”)上运行,相同的 8381 行数略低查询成本(940 -> 854)

使用原始索引(“class_id”、“student_id”、“created_at”)应用第一个查询会产生:

FLUSH STATUS;
    explain FORMAT=JSON SELECT MAX(`id`) `0` FROM `studenthistory`
    WHERE `class_id`=1 AND `created_at`>='2021-11-05T18:25:50.544850+00:00'
    GROUP BY `student_id`;

| {
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "858.94"
    },
    "grouping_operation": {
      "using_filesort": false,
      "table": {
        "table_name": "studenthistory",
        "access_type": "ref",
        "possible_keys": [
          "fk_studenthistory_student_e25b0310",
          "idx_studenthistory_class_id_931474"
        ],
        "key": "idx_studenthistory_class_id_931474",
        "used_key_parts": [
          "class_id"
        ],
        "key_length": "4",
        "ref": [
          "const"
        ],
        "rows_examined_per_scan": 8381,
        "rows_produced_per_join": 2793,
        "filtered": "33.33",
        "using_index": true,
        "cost_info": {
          "read_cost": "20.84",
          "eval_cost": "279.34",
          "prefix_cost": "858.94",
          "data_read_per_join": "130K"
        },
        "used_columns": [
          "id",
          "created_at",
          "student_id",
          "class_id"
        ],
        "attached_condition": "(`test-table`.`studenthistory`.`created_at` >= TIMESTAMP'2021-11-05 18:25:50.54485')"
      }
    }
  }
} |

这次的成本是 858,行 "rows_examined_per_scan": 8381, "rows_produced_per_join": 2793。但是只有 class_id 被用作键。 (为什么。?)不是剩余的 student_id 和 created_at

【问题讨论】:

  • 你很困惑。不要再混淆了。如果查询产生正确的结果:它可能是正确的。性能不是问题。正确性是。
  • 你为什么有and time(created_at) &gt; TIME('00:00:00')?为什么要排除在任何日期恰好在午夜创建的记录?
  • 我相信您的复合索引应该允许在不扫描的情况下完成查询。也许更好的是(student_id, class_id, id)
  • “MAX(id) 已经在 class_id 和 student_id 上建立索引”是什么意思? MAX(id) 不是表格;它不能被索引。您的意思是说 studenthistory 已在这些列上编入索引吗?
  • @Barmar,那个时间只是一个例子(以及表名和列名)。这个查询将在每秒插入90k个设备(传感器数据)的表上运行~每 10 秒一次,具体取决于设备类型。在实际情况下,这将是“最后 12 小时”。谢谢。

标签: mysql sql database


【解决方案1】:

查询:

select MAX(id) from studenthistory 
where class_id = 1 
group by student_id

如果你创建索引会很快:

create index ix1 on studenthistory (class_id, student_id, id);

【讨论】:

  • 你是对的!惊人的。只是为了检查 - 因为每天这张桌子上都会有很多数据。如果我像这样使用 Max(id),我将不需要 created_at 文件管理器?我不希望查询扫描特定学生下的所有记录(实际上,这是一个传感器设备 ID)以查找最大 ID。如果 Max 函数是 O(1) 时间运算,那么这将是完美的。谢谢。
  • @zcahfg2 如果您的查询变得很慢,因为您每次都读取大量行,那么您可以使用按日期过滤来减少查询读取的行数。然后将修改查询以添加此过滤器,并且需要相应地修改索引。
【解决方案2】:

查询 1

select MAX(id) from studenthistory 
where class_id = 1 
   and date(created_at) = '2021-11-05' 
   and time(created_at) > TIME('04:00:00') 
group by student_id

不要拆分日期;改为

AND created_at > '2021-11-05 04:00:00'

如果您想检查当天“创建”的行,请使用一些东西

AND created_at >= '2021-11-05'
AND created_at  < '2021-11-05' + INTERVAL 1 DAY

或者,如果您想检查“今天”:

AND created_at >= CURDATE()

今天凌晨 4 点以后:

AND created_at >= CURDATE() + INTERVAL 4 HOUR

使用date(created_at) 会使INDEXcreated_at 部分无法使用。 (参见“可分析的”)

select MAX(id) ... group by student_id

可能会返回多行——每个学生一个。也许您想摆脱group by?或者指定一个特定的student_id

查询 2 可能运行得更快:

select MAX(id) from studenthistory 
where class_id = 1 
group by student_id

但是最优索引是INDEX(class_id, student_id, id),(可以同时包含两个复合索引。)

它可能会返回多行,所以也许你想要

select student_id, MAX(id) from studenthistory 
where class_id = 1 
group by student_id

最大

我相信 MAX 会简单地获取最后一个值而不检查整行,如果它被索引的话。

有时。

您的第二个查询可以做到这一点。但是第一个查询不能——因为范围测试(在 created_at 上)正在阻碍。

解释

查询计划似乎...类似

唉,EXPLAIN 忽略了细节。您可以通过EXPLAIN FORMAT=JSON SELECT ... 获得更多详细信息,但不一定足够详细。

我想您会发现,在添加我建议的索引后,第二个查询会为“Rows”提供更小的值。

一种准确测量“接触的行(表或索引)”的方法:

FLUSH STATUS;
SELECT ...;
SHOW SESSION STATUS LIKE 'Handler%';

传感器数据

对于传感器数据,考虑多个表:

  • 原始数据(“事实”表,在数据仓库术语中)。每个传感器的每个读数都有一行。
  • 每个传感器的最新值。每个 90K 传感器都有一行。维护这张表比为每个传感器“找到最新”值要容易得多;这是一个“分组最大”问题。
  • 汇总数据。一个示例是为每个传感器设置高/低/平均/等值。每个传感器每小时(或每天或任何有用的)一行。

【讨论】:

  • 查询只有 MAX(ID),因为它是子查询的一部分 - 我试图只提供 stackoverflow 指南建议的“最小可重现示例”,但它似乎引起了更多的混乱我希望。我为此道歉。为了提供更多上下文,student_id 实际上是一个传感器 device_id,每个传感器每 1-10 秒插入一次此表。 (大约 9 万台设备)
  • 这将是没有日期时间的完整查询(查询 2 作为子查询)。即获取给定传感器类(本例中为 1)中每个传感器的最新数据,``` SELECT * FROM studenthistory WHERE id IN(从 studenthistory 中选择 MAX(id) where class_id = 1 group by student_id) ```
  • 你的回答很好,我学到了很多东西——尤其是日期函数不是 sargable 并且查询 2 的性能优于查询 1,因为首先——它被索引,其次,如果索引是 @ 987654340@,那么 MAX(id) 将是 O(1) 操作。非常感谢您的回答。关于 FLUSH STATUS 和 EXPLAIN FORMAT=JSON 的提示很棒,我现在就运行它。
  • 我在问题中添加了查询计划。奇怪的是(解释 FORMAT=JSON SELECT MAX(id) 0 FROM studenthistory WHERE class_id=1 AND created_at>='2021-11-05T18:25:50.544850+00:00' GROUP BY student_id;) 此查询仅使用第一个 class_id 索引,而不是剩余的 student_id 和 created_at 索引。
  • @zcahfg2 - 我正在写一篇关于如何使用“传感器数据”的博客,所以我实际上更乐意谈论真正的问题,而不是“最小的可重现示例”。我添加到我的答案中。请注意,我偏离了您最初的问题——因为您的原始问题似乎以不同的方式得到了更好的回答。
猜你喜欢
  • 2012-11-15
  • 1970-01-01
  • 2015-01-23
  • 1970-01-01
  • 2015-11-03
  • 1970-01-01
  • 2013-01-28
  • 2012-05-26
  • 2021-01-08
相关资源
最近更新 更多