V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
MySQL 5.5 Community Server
MySQL 5.6 Community Server
Percona Configuration Wizard
XtraBackup 搭建主从复制
Great Sites on MySQL
Percona
MySQL Performance Blog
Severalnines
推荐管理工具
Sequel Pro
phpMyAdmin
推荐书目
MySQL Cookbook
MySQL 相关项目
MariaDB
Drizzle
参考文档
http://mysql-python.sourceforge.net/MySQLdb.html
myd
V2EX  ›  MySQL

时间范围查询如何优化?

  •  
  •   myd · 2021-02-23 18:22:02 +08:00 · 3536 次点击
    这是一个创建于 1150 天前的主题,其中的信息可能已经有所发展或是发生改变。

    以用户上线 /下线记录表为例:

    CREATE TABLE `online_log` (
      `id` int(11) AUTO_INCREMENT,
      `user_id` int(11),
      `online_time` datetime COMMENT '上线时间',
      `offline_time` datetime COMMENT '下线时间',
      PRIMARY KEY (`id`)
    );
    

    查询某个时间点在线的用户:

    select user_id from online_log where "2021-01-23 12:00:00" between online_time and offline_time;
    

    存在的问题:

    1. 加联合索引(online_time, offline_time)显然不行,用不上;
    2. 加 2 个普通索引 online_time 和 offline_time,只能用一个,也有可能都用不上,扫描的记录很多,速度慢。

    如何优化?

    16 条回复    2021-02-24 17:39:44 +08:00
    Altale
        1
    Altale  
       2021-02-23 18:37:03 +08:00
    方案没有,但是提个小建议,第二点问题可能是错的

    “加 2 个普通索引 online_time 和 offline_time,只能用一个,也有可能都用不上”

    这个方案最理想的情况是两个索引都使用上,最终取交集,具体情况要看 DBMS 的统计数据认为交集的方案开销是否比其他查询路径小。

    另一个小建议是如果你这个表很大,还有其他字段,那么加这两个索引:
    - online_time - user_id
    - offline_time - user_id

    将取交集的操作继续缩小为不需要回表的方案,性能可能有大幅提升(安利测试看看)
    rund11
        2
    rund11  
       2021-02-23 18:42:38 +08:00 via Android
    多加几个字段年月日,时间点,多个索引会快
    qiayue
        3
    qiayue  
       2021-02-23 18:42:56 +08:00
    用时间戳会不会好一点呢
    JustLookBy
        4
    JustLookBy  
       2021-02-23 18:49:44 +08:00   ❤️ 1
    联合索引怎么会用不上。
    查询的时候 online_time<'' and offline_time>'' 不就是正常联合索引的操作吗?
    546L5LiK6ZOt
        5
    546L5LiK6ZOt  
       2021-02-23 18:51:31 +08:00
    我理解联合索引用不上,是因为两个字段的比较是不一样的,一个大于一个小于( online_time <= "2021-01-23 12:00:00" and offline_time >= "2021-01-23 12:00:00")。那么可以来个骚操作,变成一样的。假设 online_time 和 offline_time 都是存时间戳,但 offline_time 存的是时间戳的负数。那么条件就变成了 online_time <= 时间戳 and offline_time <= 时间戳的负数 。这样子就可以建联合索引了。
    myd
        6
    myd  
    OP
       2021-02-23 18:51:31 +08:00
    @Altale 是的,数据量小会取交集。但是数据量大的时候,很可能就是全表扫描。MySQL 数据库
    myd
        7
    myd  
    OP
       2021-02-23 18:54:12 +08:00
    @rund11 但是上线时间和下线时间可能跨日,甚至跨月,跨年
    myd
        8
    myd  
    OP
       2021-02-23 18:58:16 +08:00   ❤️ 1
    @JustLookBy
    @546L5LiK6ZOt
    联合索引用不上,是因为,根据最左前缀原则,只要有一个索引字段使用了范围查询(>、<、<=、>=),后面的索引字段就不生效了。
    Huelse
        9
    Huelse  
       2021-02-23 19:18:51 +08:00
    两个日期之间的天数用 DATEDIFF()怎么样?
    Huelse
        10
    Huelse  
       2021-02-23 19:22:43 +08:00
    还有什么 timediff 、datediff 、timestampdiff,可以测试下性能
    Altale
        11
    Altale  
       2021-02-24 12:10:52 +08:00
    @myd 那提供的条件太有限了,无法知道具体的查询 pattern,范围,这些建议都给不了
    Altale
        12
    Altale  
       2021-02-24 12:20:22 +08:00
    其实让数据库执行你的查询,同一条 sql 在数据分布不同的情况下执行路径也会大有不同,问题太过 general 了,问人还不如相信数据库的分析
    JustLookBy
        13
    JustLookBy  
       2021-02-24 12:48:29 +08:00
    @myd 我想当然了,复合索引确实不能用
    你这问题可以用空间索引 spatial index 来解决。
    具体方案如下:
    1. 去掉 online offline 俩个字段,用 online_range 来表示在线时间访问,type 为 linestring.
    2. 加入索引 online_range, index type 设为 spatial
    3. 用 int 表示时间,自己设定一个开始时间 为 0,这个方案如果精确度到秒,那区间只能有十几年。2^32/86400/365
    4. 插入数据 `insert log (online_range) values (ST_GeomFromText('LINESTRING(online_timestamp 0,offline_timestamp 0)')))。 这里你的情况只要线段范围就行,所以 y 左边都设为 0 即可。
    5. 查询数据 ` select * from log where
    MBRContains(online_range,ST_GeomFromText('point(28302301 0)'))`
    myd
        14
    myd  
    OP
       2021-02-24 16:05:47 +08:00
    @JustLookBy

    有点像 online_time 和 offline_time,数据量少的时候可以使用索引。

    数据量 100w 时,也是全表扫描。SQL:
    ```sql
    EXPLAIN
    select * from online_log where
    MBRContains(online_range,ST_GeomFromText('point(1614152940 0)'));

    +----+-------------+------------+------------+------+--------------------+------+---------+------+---------+----------+-------------+
    | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
    +----+-------------+------------+------------+------+--------------------+------+---------+------+---------+----------+-------------+
    | 1 | SIMPLE | online_log | NULL | ALL | index_online_range | NULL | NULL | NULL | 1343396 | 31.03 | Using where |
    +----+-------------+------------+------------+------+--------------------+------+---------+------+---------+----------+-------------+
    1 row in set, 1 warning (0.00 sec)
    ```
    这条 SQL 最后筛选出来的数据大约 100 条。


    优化过程:
    ```
    ......
    "analyzing_range_alternatives": {
    "range_scan_alternatives": [
    {
    "index": "index_online_range",
    "ranges": [
    "online_range unprintable_geometry_value"
    ],
    "index_dives_for_eq_ranges": true,
    "rowid_ordered": false,
    "using_mrr": false,
    "index_only": false,
    "rows": 425368,
    "cost": 510443,
    "chosen": false,
    "cause": "cost"
    }
    ],
    "analyzing_roworder_intersect": {
    "usable": false,
    "cause": "too_few_roworder_scans"
    }
    }
    ......
    ```
    JustLookBy
        15
    JustLookBy  
       2021-02-24 17:17:19 +08:00
    @myd 不能直接用时间戳,值大于 2^32 就不能用到索引了。 我在第三点里面说了,但是没说清
    ```自己设定一个开始时间 为 0,这个方案如果精确度到秒,那区间只能有十几年。```
    你百万数据查询耗时多少?我这是一秒内,结果是十万条左右
    myd
        16
    myd  
    OP
       2021-02-24 17:39:44 +08:00
    @JustLookBy 现在我用的时间戳是 4 字节的 int,没有超过 2^32 。百万数据 1 秒是正常的,全表扫描就是这个速度。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   5219 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 07:03 · PVG 15:03 · LAX 00:03 · JFK 03:03
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.