处理访问数 table 中数百万行的最佳方法是什么?
What is the best way to handle millions of rows inside the Visits table?
根据 ,答案是正确的并且使查询更好,但并没有解决整个问题。
CREATE TABLE `USERS` (
`ID` char(255) COLLATE utf8_unicode_ci NOT NULL,
`NAME` char(255) COLLATE utf8_unicode_ci NOT NULL,
PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
USERS里面只有5行table.
ID
NAME
C9XzpOxWtuh893z1GFB2sD4BIko2
...
I2I7CZParyMatRKnf8NiByujQ0F3
...
EJ12BBKcjAr2I0h0TxKvP7uuHtEg
...
VgqUQRn3W6FWAutAnHRg2K3RTvVL
...
M7jwwsuUE156P5J9IAclIkeS4p3L
...
CREATE TABLE `VISITS` (
`USER_ID` char(255) COLLATE utf8_unicode_ci NOT NULL,
`VISITED_IN` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY `USER_ID` (`USER_ID`,`VISITED_IN`),
CONSTRAINT `VISITS_ibfk_1` FOREIGN KEY (`USER_ID`) REFERENCES `USERS` (`ID`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
VISITS里面的索引table:
Keyname
Type
Unique
Packed
Column
Cardinality
Collation
Null
Comment
USER_ID
BTREE
No
No
USER_ID
VISITED_IN
3245
5283396
A
A
No
No
VISITS 中有 5,740,266 行 table:
C9XzpOxWtuh893z1GFB2sD4BIko2 = 4,359,264 次个人资料访问
I2I7CZParyMatRKnf8NiByujQ0F3 = 1,237,286 次个人资料访问
EJ12BBKcjAr2I0h0TxKvP7uuHtEg = 143,716 次个人资料访问
VgqUQRn3W6FWAutAnHRg2K3RTvVL = 0 次个人资料访问
M7jwwsuUE156P5J9IAclIkeS4p3L = 0 次个人资料访问
查询耗时:(秒数会根据行数变化)
SELECT COUNT(*) FROM VISITS WHERE USER_ID = C9XzpOxWtuh893z1GFB2sD4BIko2
- 在应用 Rick James 的回答之前,查询耗时 90 到 105 秒
- 应用 Rick James 的回答后,查询用了 55 到 65 秒
SELECT COUNT(*) FROM VISITS WHERE USER_ID = I2I7CZParyMatRKnf8NiByujQ0F3
- 在应用 Rick James 的回答之前,查询耗时 90 到 105 秒
- 应用 Rick James 的回答后,查询用了 20 到 30 秒
SELECT COUNT(*) FROM VISITS WHERE USER_ID = EJ12BBKcjAr2I0h0TxKvP7uuHtEg
- 在应用 Rick James 的回答之前,查询耗时 90 到 105 秒
应用 Rick James 的答案后,查询耗时 4 到 8 秒
SELECT COUNT(*) FROM VISITS WHERE USER_ID = VgqUQRn3W6FWAutAnHRg2K3RTvVL
- 在应用 Rick James 的回答之前,查询耗时 90 到 105 秒
- 应用 Rick James 的回答后,查询用了 1 到 3 秒
SELECT COUNT(*) FROM VISITS WHERE USER_ID = M7jwwsuUE156P5J9IAclIkeS4p3L
- 在应用 Rick James 的回答之前,查询耗时 90 到 105 秒
- 应用 Rick James 的回答后,查询用了 1 到 3 秒
正如您在应用索引之前看到的那样,计算特定用户的访问需要 90 到 105 秒,即使该用户有几行(访问)也是如此。
应用索引后情况变得更好,但问题是:
- 如果我访问
C9XzpOxWtuh893z1GFB2sD4BIko2
配置文件,需要
在 55 到 65 秒之间获得个人资料访问。
- 如果我访问
I2I7CZParyMatRKnf8NiByujQ0F3
个人资料,需要
在 20 到 30 秒之间获得个人资料访问。
- 等等...
拥有几行(访问)的用户会很幸运,因为他的个人资料加载速度会更快。
我可以忽略上面的所有内容并在 USERS table 中创建一个列来计算用户访问并在捕获新访问时增加它而无需创建数百万行,但这对我不起作用,因为我允许用户像这样过滤访问:
最后 60 分钟
过去 24 小时
最近 7 天
最近 30 天
最近 6 个月
过去 12 个月
历来
我该怎么办?
这不是答案,而是建议。
- 如果他们不需要 real-time 数据,
难道我们不能 运行 一个调度程序并每隔 x 分钟将它们插入摘要 table 中。然后我们可以访问该摘要 table 以供您统计。
注意:如果您需要 time-wise 登录计数,我们可以在您的 table 中添加一个 同步时间 列。 (然后你的总结table也会动态增加)
Table 列例如:
PK_Column,用户ID,访问次数,sync_time
我们可以为您的前端使用异步(反应式)实现。这意味着,数据将在一段时间后加载,但用户永远不会在工作中遇到这种延迟。
创建一个摘要 table 和每天中午 12 点 运行 一个作业,然后将用户明智和日期明智的最后一次访问的夏季放入 table。
user_visit_Summary Table:
PK_Column, User ID, Number_of_Visites, VISIT_Date
注意:为用户 ID 和日期字段创建索引
当您检索数据时,您将通过数据库函数访问它
Select count(*) + (Select Number_of_Visites from VISITS
where user_id = xxx were VISIT_Date <= ['DATE 12:00 AM' -1] PK_Column desc limit 1) as old_visits
where USER_ID = xxx and VISITED_IN > 'DATE 12:00 AM';
问题是您正在评估并持续 re-evaluating,非常大的行计数实际上是历史的一部分并且永远不会改变。您不能每次都计算这些行,因为这会花费太长时间。您要为以下项目提供计数:
最后 60 分钟
过去 24 小时
过去 7 天
过去 30 天
过去六个月
All-time
您需要四个 table:
Table 1:小而快table保存今天和昨天的访问记录
Table 2:一个更小的,非常快的table持有计数'前天(“D-2”)到“D-7”,字段'D2toD7'、期间 'D8toD30'、'D31toD183' 和 'D184andEarlier'
Table 3: table 保存每个用户每天的访问次数
Table 4:您已经拥有的非常大且缓慢的 table,每次访问都会根据时间戳记入日志
然后您可以通过直接查询 Table 1 来获得 'Last 60 minutes' 和 'Last 24 hours' 计数,这将非常快。
“最近 7 天”是 Table 1(对于您的用户)中所有记录的计数加上 Table 2 中的 D2toD7 值(对于您的用户)。
“最近 30 天”是 Table 1(对于您的用户)加上 D2 到 D7,再加上 D8 到 D30 中的所有记录的计数。
“最近六个月”是 Table 1 加上 D2 到 D7,加上 D8 到 D30,再加上 D31 到 D183。
‘All-time’是Table1加D2toDy,加D8toD30,加D31toD183,加D184andEarlier。
我会 运行宁 php 脚本来检索这些值——没有必要尝试在一个复杂的查询中完成所有操作。数次,甚至数次,非常快速地点击数据库,收集数字,return 结果。该脚本将在不到一秒的时间内 运行。
那么,如何更新 Table 2 中的计数?这是您需要 Table 3 的地方,它保存每个用户每天的访问次数。创建 Table 3 并使用所有访问的巨大 table 数据的 COUNT 值填充它,按用户和日期分组,这样您就可以得到每个用户每天的访问次数。您只需要创建和填充 Table 3 一次。
您现在需要每天一次 CRON job/script 或类似 运行。此脚本将从 Table 1 中删除记录前天访问的行。此脚本需要:
- 确定每个用户前天的访问次数
- 将这些计数插入 Table 3 中的“前天”日期。
- 将计数值添加到 Table 中每个用户的“D2toD7”值 2.
- 从 Table 1.
中删除 'day before yesterday' 行
- 在 Table 中为每个用户查找(刚刚变成的)D8 的值 3. 为每个用户将此值从“D2 到 D7”值递减。
- 对于“D8toD30”、“D31toD183”等字段中的每一个,为现在属于时间段的那一天递增,根据退出时间段的那一天递减。使用存储在 Table 中的值 3.
切记分寸;对于任何 real-world 访问计数目的,183 天的时间段大约为六个月。
概述:您无法快速计算数百万行。使用这些是永远不会改变的历史人物这一事实。因为 Table 1 用于 up-to-the-minute 计数,所以您每天只需要更新一次历史周期计数。多个(甚至几十个)非常非常快的查询将很快为您提供准确的结果。
对于一天或更长时间的任何查询,请使用摘要 table。
即构建并维护一个包含 3 列 user_id、日期、计数的摘要 table; PRIMARY KEY(user_id, date)
对于“所有时间”和“上个月”,查询将是
SELECT CUM(count) FROM summary WHERE user_id='...';
SELECT CUM(count) FROM summary
WHERE user_id='...'
AND date >= CURDATE() - INTERVAL 1 MONTH
每晚午夜,将摘要 table 中每个用户的当前 table 滚动到一行,然后清除 table。此 table 将继续用于更短的时间跨度。
这实现了每个用户在每个时间范围内的速度。
但是,有一个“错误”。我强迫“天”/“周”/等从午夜到午夜,不允许你真正说“过去 24 小时”。
我建议针对该“错误”采取以下折衷方案:
- 对于较长的时间跨度,使用摘要 table,加上今天从其他 table 获得的点击数。
- 为了让“24 小时”回到昨天,将另一个 table 改成回到昨天早上。也就是说,仅在 24 小时后清除,而不是 1 个日历日。
要一次获取所有计数器,请在子查询中完成所有工作。有两种方法,可能同样快,但结果要么在行中,要么在列中:
-- rows:
SELECT 'hour', COUNT(*) FROM recent ...
UNION ALL
SELECT '24 hr', COUNT(*) FROM recent ...
UNION ALL
SELECT 'month', SUM(count) FROM summary ...
UNION ALL
SELECT 'all', SUM(count) FROM summary ...
;
-- columns:
SELECT
( SELECT COUNT(*) FROM recent ... ) AS 'hour'.
( SELECT COUNT(*) FROM recent ... ) AS '24 hr',
( SELECT SUM(count) FROM summary ... ) AS 'last month'
( SELECT SUM(count) FROM summary ... ) AS 'all time'
;
“...”是
WHERE user_id = '...'
AND datetime >= ... -- except for "all time"
将几个查询合并为一个查询(无论哪种方式)都有一个优点 -- 这避免了多次往返服务器和多次调用优化器。
forpas 提供了另一种方法 但需要对其进行调整以达到两个不同的 tables。
根据
CREATE TABLE `USERS` (
`ID` char(255) COLLATE utf8_unicode_ci NOT NULL,
`NAME` char(255) COLLATE utf8_unicode_ci NOT NULL,
PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
USERS里面只有5行table.
ID | NAME |
---|---|
C9XzpOxWtuh893z1GFB2sD4BIko2 | ... |
I2I7CZParyMatRKnf8NiByujQ0F3 | ... |
EJ12BBKcjAr2I0h0TxKvP7uuHtEg | ... |
VgqUQRn3W6FWAutAnHRg2K3RTvVL | ... |
M7jwwsuUE156P5J9IAclIkeS4p3L | ... |
CREATE TABLE `VISITS` (
`USER_ID` char(255) COLLATE utf8_unicode_ci NOT NULL,
`VISITED_IN` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY `USER_ID` (`USER_ID`,`VISITED_IN`),
CONSTRAINT `VISITS_ibfk_1` FOREIGN KEY (`USER_ID`) REFERENCES `USERS` (`ID`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
VISITS里面的索引table:
Keyname | Type | Unique | Packed | Column | Cardinality | Collation | Null | Comment |
---|---|---|---|---|---|---|---|---|
USER_ID | BTREE | No | No | USER_ID VISITED_IN |
3245 5283396 |
A A |
No No |
VISITS 中有 5,740,266 行 table:
C9XzpOxWtuh893z1GFB2sD4BIko2 = 4,359,264 次个人资料访问
I2I7CZParyMatRKnf8NiByujQ0F3 = 1,237,286 次个人资料访问
EJ12BBKcjAr2I0h0TxKvP7uuHtEg = 143,716 次个人资料访问
VgqUQRn3W6FWAutAnHRg2K3RTvVL = 0 次个人资料访问
M7jwwsuUE156P5J9IAclIkeS4p3L = 0 次个人资料访问
查询耗时:(秒数会根据行数变化)
SELECT COUNT(*) FROM VISITS WHERE USER_ID = C9XzpOxWtuh893z1GFB2sD4BIko2
- 在应用 Rick James 的回答之前,查询耗时 90 到 105 秒
- 应用 Rick James 的回答后,查询用了 55 到 65 秒
SELECT COUNT(*) FROM VISITS WHERE USER_ID = I2I7CZParyMatRKnf8NiByujQ0F3
- 在应用 Rick James 的回答之前,查询耗时 90 到 105 秒
- 应用 Rick James 的回答后,查询用了 20 到 30 秒
SELECT COUNT(*) FROM VISITS WHERE USER_ID = EJ12BBKcjAr2I0h0TxKvP7uuHtEg
- 在应用 Rick James 的回答之前,查询耗时 90 到 105 秒 应用 Rick James 的答案后,查询耗时 4 到 8 秒
SELECT COUNT(*) FROM VISITS WHERE USER_ID = VgqUQRn3W6FWAutAnHRg2K3RTvVL
- 在应用 Rick James 的回答之前,查询耗时 90 到 105 秒
- 应用 Rick James 的回答后,查询用了 1 到 3 秒
SELECT COUNT(*) FROM VISITS WHERE USER_ID = M7jwwsuUE156P5J9IAclIkeS4p3L
- 在应用 Rick James 的回答之前,查询耗时 90 到 105 秒
- 应用 Rick James 的回答后,查询用了 1 到 3 秒
正如您在应用索引之前看到的那样,计算特定用户的访问需要 90 到 105 秒,即使该用户有几行(访问)也是如此。
应用索引后情况变得更好,但问题是:
- 如果我访问
C9XzpOxWtuh893z1GFB2sD4BIko2
配置文件,需要 在 55 到 65 秒之间获得个人资料访问。 - 如果我访问
I2I7CZParyMatRKnf8NiByujQ0F3
个人资料,需要 在 20 到 30 秒之间获得个人资料访问。 - 等等...
拥有几行(访问)的用户会很幸运,因为他的个人资料加载速度会更快。
我可以忽略上面的所有内容并在 USERS table 中创建一个列来计算用户访问并在捕获新访问时增加它而无需创建数百万行,但这对我不起作用,因为我允许用户像这样过滤访问:
最后 60 分钟
过去 24 小时
最近 7 天
最近 30 天
最近 6 个月
过去 12 个月
历来
我该怎么办?
这不是答案,而是建议。
- 如果他们不需要 real-time 数据, 难道我们不能 运行 一个调度程序并每隔 x 分钟将它们插入摘要 table 中。然后我们可以访问该摘要 table 以供您统计。
注意:如果您需要 time-wise 登录计数,我们可以在您的 table 中添加一个 同步时间 列。 (然后你的总结table也会动态增加)
Table 列例如:
PK_Column,用户ID,访问次数,sync_time
我们可以为您的前端使用异步(反应式)实现。这意味着,数据将在一段时间后加载,但用户永远不会在工作中遇到这种延迟。
创建一个摘要 table 和每天中午 12 点 运行 一个作业,然后将用户明智和日期明智的最后一次访问的夏季放入 table。
user_visit_Summary Table: PK_Column, User ID, Number_of_Visites, VISIT_Date
注意:为用户 ID 和日期字段创建索引
当您检索数据时,您将通过数据库函数访问它
Select count(*) + (Select Number_of_Visites from VISITS
where user_id = xxx were VISIT_Date <= ['DATE 12:00 AM' -1] PK_Column desc limit 1) as old_visits
where USER_ID = xxx and VISITED_IN > 'DATE 12:00 AM';
问题是您正在评估并持续 re-evaluating,非常大的行计数实际上是历史的一部分并且永远不会改变。您不能每次都计算这些行,因为这会花费太长时间。您要为以下项目提供计数:
最后 60 分钟
过去 24 小时
过去 7 天
过去 30 天
过去六个月
All-time
您需要四个 table:
Table 1:小而快table保存今天和昨天的访问记录
Table 2:一个更小的,非常快的table持有计数'前天(“D-2”)到“D-7”,字段'D2toD7'、期间 'D8toD30'、'D31toD183' 和 'D184andEarlier'
Table 3: table 保存每个用户每天的访问次数
Table 4:您已经拥有的非常大且缓慢的 table,每次访问都会根据时间戳记入日志
然后您可以通过直接查询 Table 1 来获得 'Last 60 minutes' 和 'Last 24 hours' 计数,这将非常快。 “最近 7 天”是 Table 1(对于您的用户)中所有记录的计数加上 Table 2 中的 D2toD7 值(对于您的用户)。 “最近 30 天”是 Table 1(对于您的用户)加上 D2 到 D7,再加上 D8 到 D30 中的所有记录的计数。 “最近六个月”是 Table 1 加上 D2 到 D7,加上 D8 到 D30,再加上 D31 到 D183。 ‘All-time’是Table1加D2toDy,加D8toD30,加D31toD183,加D184andEarlier。
我会 运行宁 php 脚本来检索这些值——没有必要尝试在一个复杂的查询中完成所有操作。数次,甚至数次,非常快速地点击数据库,收集数字,return 结果。该脚本将在不到一秒的时间内 运行。
那么,如何更新 Table 2 中的计数?这是您需要 Table 3 的地方,它保存每个用户每天的访问次数。创建 Table 3 并使用所有访问的巨大 table 数据的 COUNT 值填充它,按用户和日期分组,这样您就可以得到每个用户每天的访问次数。您只需要创建和填充 Table 3 一次。 您现在需要每天一次 CRON job/script 或类似 运行。此脚本将从 Table 1 中删除记录前天访问的行。此脚本需要:
- 确定每个用户前天的访问次数
- 将这些计数插入 Table 3 中的“前天”日期。
- 将计数值添加到 Table 中每个用户的“D2toD7”值 2.
- 从 Table 1. 中删除 'day before yesterday' 行
- 在 Table 中为每个用户查找(刚刚变成的)D8 的值 3. 为每个用户将此值从“D2 到 D7”值递减。
- 对于“D8toD30”、“D31toD183”等字段中的每一个,为现在属于时间段的那一天递增,根据退出时间段的那一天递减。使用存储在 Table 中的值 3.
切记分寸;对于任何 real-world 访问计数目的,183 天的时间段大约为六个月。
概述:您无法快速计算数百万行。使用这些是永远不会改变的历史人物这一事实。因为 Table 1 用于 up-to-the-minute 计数,所以您每天只需要更新一次历史周期计数。多个(甚至几十个)非常非常快的查询将很快为您提供准确的结果。
对于一天或更长时间的任何查询,请使用摘要 table。
即构建并维护一个包含 3 列 user_id、日期、计数的摘要 table; PRIMARY KEY(user_id, date)
对于“所有时间”和“上个月”,查询将是
SELECT CUM(count) FROM summary WHERE user_id='...';
SELECT CUM(count) FROM summary
WHERE user_id='...'
AND date >= CURDATE() - INTERVAL 1 MONTH
每晚午夜,将摘要 table 中每个用户的当前 table 滚动到一行,然后清除 table。此 table 将继续用于更短的时间跨度。
这实现了每个用户在每个时间范围内的速度。
但是,有一个“错误”。我强迫“天”/“周”/等从午夜到午夜,不允许你真正说“过去 24 小时”。
我建议针对该“错误”采取以下折衷方案:
- 对于较长的时间跨度,使用摘要 table,加上今天从其他 table 获得的点击数。
- 为了让“24 小时”回到昨天,将另一个 table 改成回到昨天早上。也就是说,仅在 24 小时后清除,而不是 1 个日历日。
要一次获取所有计数器,请在子查询中完成所有工作。有两种方法,可能同样快,但结果要么在行中,要么在列中:
-- rows:
SELECT 'hour', COUNT(*) FROM recent ...
UNION ALL
SELECT '24 hr', COUNT(*) FROM recent ...
UNION ALL
SELECT 'month', SUM(count) FROM summary ...
UNION ALL
SELECT 'all', SUM(count) FROM summary ...
;
-- columns:
SELECT
( SELECT COUNT(*) FROM recent ... ) AS 'hour'.
( SELECT COUNT(*) FROM recent ... ) AS '24 hr',
( SELECT SUM(count) FROM summary ... ) AS 'last month'
( SELECT SUM(count) FROM summary ... ) AS 'all time'
;
“...”是
WHERE user_id = '...'
AND datetime >= ... -- except for "all time"
将几个查询合并为一个查询(无论哪种方式)都有一个优点 -- 这避免了多次往返服务器和多次调用优化器。
forpas 提供了另一种方法