【问题标题】:MySQL - Select the least day of the current month/year, not necessarily the first day of the monthMySQL - 选择当前月/年的最少一天,不一定是当月的第一天
【发布时间】:2012-07-14 12:40:46
【问题描述】:

使用 PHP/MySQL

我正在尝试创建一个 select 语句,该语句从当前月份和年份的最少一天获取数据(我正在使用它来显示来自某个玩家“本月”的数据)。第一个月份的数据可能并不总是存在,因此如果没有找到第一个,那么它将使用第二个或第三个等,以最小的月份为准。

查询需要类似于:

SELECT * FROM table_name WHERE name = '$username' AND
[...theDate = the least day of data in the current month/year]

它将返回单行数据。

这是我用于获取“本周”数据的查询:

SELECT name, data, theDate
FROM table_name
WHERE name = '$username'
AND YEARWEEK(theDate) = YEARWEEK(CURRENT_DATE)
ORDER BY theDate;

【问题讨论】:

    标签: php mysql database


    【解决方案1】:

    这是一个简单的解决方案:

    SELECT a.*
    FROM table_name a
    INNER JOIN
    (
        SELECT name, MIN(theDate) AS mindate
        FROM table_name
        WHERE MONTH(theDate) = MONTH(CURDATE()) AND
              YEAR(theDate) = YEAR(CURDATE()) AND
              name = '$username'
        GROUP BY name
    ) b ON a.name = b.name AND a.theDate = b.mindate
    ORDER BY theDate
    

    内部子选择首先选择当前月份和当前年份的最小日期。这应该只返回一个结果。

    然后,外部查询加入这一结果,从而获得当前月份和年份的最早记录日期的行。

    假设每个玩家每天只生成一行,我们可以进一步简化:

    SELECT *
    FROM table_name
    WHERE MONTH(theDate) = MONTH(CURDATE()) AND
          YEAR(theDate) = YEAR(CURDATE()) AND
          name = '$username'
    ORDER BY theDate
    LIMIT 1
    

    【讨论】:

    • 我还没有尝试过,因为其他解决方案对我有用,但感谢您的帮助。
    • 您的内部查询还需要检查正确的name。 (如果我们正在寻找 2012 年 7 月 14 日加入的用户“Foo”,但用户“Bar”的记录可以追溯到 2012 年 7 月 1 日呢?)
    • @pilcrow,+1 好收获。感谢您指出我的错误。
    • @Zane:只是指出,在theDate 周围使用 YEAR() 和 MONTH() 函数,MySQL 无法进行索引范围扫描来满足这些谓词。 (我确定`MONTH(theDate) = val` 是不可分割的,我几乎可以肯定YEAR(theDate) = val 也是不可分割的。我说几乎可以肯定,因为可以想象,MySQL 可以通过重写在某个时候改变它作为范围扫描。我们可以通过在裸列上使用范围扫描谓词来允许 MySQL 使用索引范围扫描操作。
    • 只做一次。 MySQL 只需执行一次,因为该表达式是确定性的,并且不会因任何行而异。如果我们替换 NOW() 而是引用表中的一列,这是一个不同的故事(当然)......这就是我们遇到“必须为每一行执行”的地方......这就是为什么我们把一侧是裸列,另一侧使用文字进行所有旋转(NOW() 函数本质上为我们返回了语句的文字。
    【解决方案2】:

    如果您只想返回一行,最简单的方法是:

    SELECT t.*
      FROM table_name t
     WHERE t.name = '$username'
       AND t.theDate >= CAST(DATE_FORMAT(NOW(),'%Y-%m-01') AS DATE)
       AND t.theDate < DATE_ADD(DATE_FORMAT(NOW(),'%Y-%m-01'), INTERVAL 1 MONTH)
     ORDER BY s.name DESC, s.theDate DESC
     LIMIT 1
    

    ORDER BY 假设您在(name,theDate) 上(或前导列)有一个索引。这将是谓词的最合适的索引(即 WHERE 子句中的条件。我们真的不需要对 name 列进行排序,因为我们知道它将等于某个东西......但是指定 ORDER BY这样一来,MySQL 更有可能对索引进行反向扫描操作,以正确的顺序返回行,避免文件排序操作。

    注意:我在 WHERE 子句的条件中指定了 theDate 裸列,而不是将其包装在任何函数中...通过指定裸列和有界范围,我们使 MySQL 能够使用索引范围扫描操作。还有其他可能的方法可以在 WHERE 子句中包含此条件,例如...

    DATE_FORMAT(t.theDate,'%Y-%m') = DATE_FORMAT(NOW(),'%Y-%m')
    

    这将返回一个等价的结果,但是这样的谓词是不可分割的。也就是说,MySQL 不能/不会对索引进行范围扫描来满足这一点。

    如果您打算为给定用户获取一个月内“最少”日期的所有行(您的问题似乎并不表明您只需要一行),这是获得该结果的一种方法:

    SELECT t.* 
      FROM table_name t
      JOIN ( SELECT s.name
                  , s.theDate
               FROM table_name s 
              WHERE s.name = '$username'
                AND s.theDate >= CAST(DATE_FORMAT(NOW(),'%Y-%m-01') AS DATE)
                AND s.theDate < DATE_ADD(DATE_FORMAT(NOW(),'%Y-%m-01'), INTERVAL 1 MONTH)
              ORDER BY s.name DESC, s.theDate DESC
              LIMIT 1
           ) r
        ON r.name = t.name
       AND r.theDate = t.theDate 
    

    再一次,MySQL 可以使用带有前导列 (name,theDate) 的索引(如果可用)来满足谓词,并执行反向扫描操作(避免排序)和执行 JOIN 操作。

    注意:我们在这里假设“theDate”是数据类型 DATE(没有时间组件)。如果它是 DATETIME 或 TIMESTAMP,则可能存在时间分量,并且如果具有相同“日期”的行的时间分量不同,则该查询可能不会返回给定“日期”值的所有行。 (例如,'2012-07-13 17:30' 和 '2012-07-13 19:55' 是不同的日期时间值。)如果我们想要返回这两行(因为两者都是“7 月 13 日”的日期) ,我们需要进行范围扫描而不是相等测试。

    SELECT t.* 
      FROM table_name t
      JOIN ( SELECT s.name
                  , s.theDate
               FROM table_name s 
              WHERE s.name = '$username'
                AND s.theDate >= CAST(DATE_FORMAT(NOW(),'%Y-%m-01') AS DATE)
                AND s.theDate < DATE_ADD(DATE_FORMAT(NOW(),'%Y-%m-01'), INTERVAL 1 MONTH)
              ORDER BY s.name DESC, s.theDate DESC
              LIMIT 1
           ) r
        ON t.name = r.name 
       AND t.theDate >= r.theDate
       AND t.theDate < DATE_FORMAT(DATE_ADD(r.theDate,INTERVAL 1 DAY),'%Y-%m-%d')
    

    请注意最后两行...我们正在寻找任何具有theDate 值的行,该值大于或等于为当前月份找到的“最小”值,并且还小于该月的午夜次日。

    【讨论】:

    • 这看起来对我有用,谢谢斯宾塞!我使用的查询类似于:“SELECT * FROM player WHERE playerName = '$username' AND theDate >= CAST(DATE_FORMAT(NOW(), '%Y-%m-01') AS DATE) ORDER BY theDate LIMIT 1”我不能投票:(
    • 您的解决方案应该比我的解决方案更快,因为它避免了将 theDate 列传递给函数,否则它将无法使用索引。但就风格而言,我会改用BETWEEN 语法。 =)
    • @Zane...好点。我避免使用带有 DATETIME 和 TIMESTAMP 列的 BETWEEN 来获取一个月的行(例如),因为我真的想将边界指定为“每月第一天的午夜”和“下个月第一天的午夜” ,并且我只想要小于上限的值,而不是小于或等于上限的值,这就是 BETWEEN 得到的。为了有效地使用 BETWEEN,我需要从上限中减去 1 秒。对于某些数据库(SQL Server),日期时间分辨率是亚秒级的。我更喜欢lb &lt;= col &lt; ub 样式,因为它可以正常工作。
    • @Zane:只有当有合适的索引可用时,使用范围扫描谓词(如我的)的查询才会更快,并且优化器生成的执行计划实际上会更快。但是,一般来说,对于大型表,对一组窄行进行索引范围扫描通常优于全表(或全索引)扫描。
    • 只想注意我的特定表的“theDate”列具有“日期”数据类型。感谢您提供所有信息!
    猜你喜欢
    • 2013-08-27
    • 2016-02-09
    • 2014-03-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-08-02
    相关资源
    最近更新 更多