在匹配子字符串上加入 table a 到 table b 的效率低下......想法?
Inefficiency of join table a to table b on matching sub-string... thoughts?
背景:
首先,我有一个名为 patients
的 table 架构,我与 patient_id
和 alerts
相关(警报是一串字符,其中每个字符代表一些任意的 value/meaning)。其次,每个 'patient' 都是一个组 [family] 的一部分,仅基于其 patient_id 的前 6 位数字。此外,还有一些第三方依赖此数据库; 我没有设计也无法更改此 schema/datamodel 并且我无法从 MySQL.
迁移
Here is a fiddle with the data model
挑战:
现在,我需要找到患者的警报包含 !
、@
、#
、%
、^
或 &
符号及其家族成员没有。我的第一个想法是收集所有具有包含这些符号的警报的患者,删除每个 patient_id 中的最后一位数字,然后按此值分组。现在我有一个列表(出于所有意图和目的)'group_ids.' 最后,我需要扩展列表以包含每个组的家庭成员及其各自的警报字符串。
这是我目前的情况:
查询#1:
SELECT p.patient_id, p.name_first, p.name_last, p.alerts
FROM patients p
INNER JOIN (SELECT SUBSTRING(patient_id, 1, CHAR_LENGTH(patient_id) - 1) AS group_id
FROM patients
WHERE patient_id BETWEEN 1000000 AND 7999999
AND (alerts like '%!%'
OR alerts like '%@%'
OR alerts like '%#%'
OR alerts like '%\%%'
OR alerts like '%^%'
OR alerts like '%&%')
GROUP BY group_id) g
ON p.patient_id LIKE CONCAT(g.group_id, '%')
ORDER BY p.patient_id
LIMIT 30000;
Fiddle ~ 注意:fiddle 不是问题的准确表述,因为包含 table 只有28条记录。
Recordset: 80,000 ~ Results: 2188 ~ Duration: 14.321 sec ~ Fetch: 0.00 sec ~ Total: 14.321 sec
查询#2:
SELECT p.patient_id, p.name_first, p.name_last, p.alerts
FROM patients p
JOIN (SELECT DISTINCT LEFT(patient_id, 6) AS group_id
FROM patients
WHERE patient_id BETWEEN 1000000 AND 7999999
AND alerts REGEXP '[!@#%^&]') g
ON p.patient_id LIKE CONCAT(g.group_id, '%')
ORDER BY p.patient_id
LIMIT 30000;
Fiddle ~ 注意:fiddle 不是问题的准确表述,因为包含 table 只有28条记录。
Recordset: 80,000 ~ Results: 2188 ~ Duration: 4.259 sec ~ Fetch: 5.663
sec ~ Total: 9.992 sec
编辑:在添加 name_first、name_last、警报和 order by 子句后,我发现这个查询花费的时间与第一个完全相同。
问题:
我得到的列表 是 准确的,但是,它不仅需要额外的处理(我打算用 PHP 来做),而且还需要14 秒!
如果有人有更好的...或者至少可以指出更好更有效的解决方案的方向,请赐教。提前致谢。
额外学分:关于 PHP 算法的任何提示,以解决给定数据的上述问题 - 忘记语义,只需一个公式即可。
如果您计划在 PHP 中进行处理,并且患者人数为 30k,我将 select 所有按患者 ID 排序的记录,遍历所有记录并按组处理:
SELECT * FROM dataminer.patients ORDER BY patient_id;
在 PHP 中,是这样的:
$patientsWithRelevantAlert = array();
$currentGroupId = null;
while(... fetch into $row ...) {
$groupId = extractGroupId($row);
// Next group? Check relevant patient and reset group info.
if ($groupId != $currentGroupId) {
if (count($patientsWithRelevantAlert) == 1) {
// remember this patient
...
}
$patientsWithRelevantAlert = array();
$currentGroupId = $groupId;
}
if(hasRelevantAlert($row)) {
$patientsWithRelevantAlerts[] = $row;
}
}
// Don't forget the last group
if (count($patientsWithRelevantAlert) == 1) {
// remember this patient
...
}
应该够快了。
也就是说,SQL,数据建模和索引是为这些东西发明的。
如果这是作业:请确保您在提交时理解代码!
如果您只对只有一名成员的群组感兴趣,为什么不只select 只对计数 (patient_id) = 1 的群组感兴趣?
SELECT g.group_id, MAX(g.patient_id) FROM
( SELECT
SUBSTRING(patient_id, 1, CHAR_LENGTH(patient_id) - 1) AS group_id,
patient_id,
FROM dataminer.patients
WHERE ...
)
GROUP BY group_id
HAVING COUNT(patient_id) = 1
我找到了一个足够有效的解决方案如下:
SELECT p.patient_id, name_first, name_last, alerts
FROM patients p
JOIN (SELECT DISTINCT LEFT(patient_id, 6) AS group_id
FROM patients
WHERE patient_id BETWEEN 1000000 AND 7999999
AND alerts REGEXP '[!@#%^&]') g
ON LEFT(p.patient_id, 6) = g.group_id /* HERE is the simple magic */
ORDER BY p.patient_id
LIMIT 30000;
记录集:80,000 ~ 结果:2188 ~ 持续时间:0.312 秒 ~ 提取:0.062 秒 ~ 总计:0.374 秒
因为我们知道合法的 patient_ids 是 7 位数字长,我们可以通过简单地使用 LEFT(patient_id, 6)
而不是效率较低的 SUBSTRING(patient_id, 1, CHAR_LENGTH(patient_id) - 1)
来确定患者的 'group_id'(这我现在明白我本来可以写成 SUBSTRING(patient_id, 1, 6)
)。无论此处使用何种方法,真正节省的是对 ON
子句的更改。与其比较 patient_id 和 LIKE CONCAT(group_id, '%')
,为什么不直接 =
比较 Table 'p' 中 patient_id 的左 6 位?
换句话说,嵌套的 select 用于查找所有唯一的 'groups',其中至少一个成员具有所需警报符号之一。主要 select 使用此 table 来确定属于这些组的所有患者。本质上,LEFT(patient_id, 6) is == 'group_id'
我们可以保留我们的索引...唯一的额外开销是每行调用一次 LEFT()。
的另一个例子
感谢大家的帮助!
编辑:因为我将在我的 PHP 算法中使用 group_id,所以我将把它添加到 select 以高效的方式:
SELECT g.group_id, RIGHT(p.patient_id, 1) AS sub_id, name_first, name_last, alerts
FROM patients p
JOIN (SELECT DISTINCT LEFT(patient_id, 6) AS group_id
FROM patients
WHERE patient_id BETWEEN 1000000 AND 7999999
AND alerts REGEXP '[!@#%^&]') g
ON LEFT(p.patient_id, 6) = g.group_id
ORDER BY p.patient_id
LIMIT 30000;
HERE is a fiddle!~注意:这不是包含的解决方案的准确表示table只有28条记录。在更大的数据集上查看以上结果。
AND finally,我用来完成处理的 PHP 算法 ~ shoutout to @The Nail:
$cur_group_id = 0;
$members = [];
$symbol = '';
$errs = false;
while($row = $result->fetch_assoc()){
$row['alerts'] = preg_replace('/[^!@#%^&]+/i', '', $row['alerts']);
if($row['group_id'] != $cur_group_id){
if($errs){
foreach($members as $member => $data){
printf('<tr><td>%d%d</td><td>%s</td><td>%s</td><td>%s</td></tr>',
$data['group_id'],
$data['sub_id'],
$data['name_last'],
$data['name_first'],
$data['alerts']);
}
}
/* reset current group */
$cur_group_id = $row['group_id'];
$members = array();
$symbol = $row['alerts'];
$errs = false;
}
$members[] = $row;
if($row['alerts'] != $symbol || strlen($row['alerts']) > 1){
$errs = true;
}
}
总处理时间(包括查询):.6 秒!!
背景:
首先,我有一个名为 patients
的 table 架构,我与 patient_id
和 alerts
相关(警报是一串字符,其中每个字符代表一些任意的 value/meaning)。其次,每个 'patient' 都是一个组 [family] 的一部分,仅基于其 patient_id 的前 6 位数字。此外,还有一些第三方依赖此数据库; 我没有设计也无法更改此 schema/datamodel 并且我无法从 MySQL.
Here is a fiddle with the data model
挑战:
现在,我需要找到患者的警报包含 !
、@
、#
、%
、^
或 &
符号及其家族成员没有。我的第一个想法是收集所有具有包含这些符号的警报的患者,删除每个 patient_id 中的最后一位数字,然后按此值分组。现在我有一个列表(出于所有意图和目的)'group_ids.' 最后,我需要扩展列表以包含每个组的家庭成员及其各自的警报字符串。
这是我目前的情况:
查询#1:
SELECT p.patient_id, p.name_first, p.name_last, p.alerts
FROM patients p
INNER JOIN (SELECT SUBSTRING(patient_id, 1, CHAR_LENGTH(patient_id) - 1) AS group_id
FROM patients
WHERE patient_id BETWEEN 1000000 AND 7999999
AND (alerts like '%!%'
OR alerts like '%@%'
OR alerts like '%#%'
OR alerts like '%\%%'
OR alerts like '%^%'
OR alerts like '%&%')
GROUP BY group_id) g
ON p.patient_id LIKE CONCAT(g.group_id, '%')
ORDER BY p.patient_id
LIMIT 30000;
Fiddle ~ 注意:fiddle 不是问题的准确表述,因为包含 table 只有28条记录。
Recordset: 80,000 ~ Results: 2188 ~ Duration: 14.321 sec ~ Fetch: 0.00 sec ~ Total: 14.321 sec
查询#2:
SELECT p.patient_id, p.name_first, p.name_last, p.alerts
FROM patients p
JOIN (SELECT DISTINCT LEFT(patient_id, 6) AS group_id
FROM patients
WHERE patient_id BETWEEN 1000000 AND 7999999
AND alerts REGEXP '[!@#%^&]') g
ON p.patient_id LIKE CONCAT(g.group_id, '%')
ORDER BY p.patient_id
LIMIT 30000;
Fiddle ~ 注意:fiddle 不是问题的准确表述,因为包含 table 只有28条记录。
Recordset: 80,000 ~ Results: 2188 ~ Duration: 4.259 sec ~ Fetch: 5.663 sec ~ Total: 9.992 sec
编辑:在添加 name_first、name_last、警报和 order by 子句后,我发现这个查询花费的时间与第一个完全相同。
问题:
我得到的列表 是 准确的,但是,它不仅需要额外的处理(我打算用 PHP 来做),而且还需要14 秒!
如果有人有更好的...或者至少可以指出更好更有效的解决方案的方向,请赐教。提前致谢。
额外学分:关于 PHP 算法的任何提示,以解决给定数据的上述问题 - 忘记语义,只需一个公式即可。
如果您计划在 PHP 中进行处理,并且患者人数为 30k,我将 select 所有按患者 ID 排序的记录,遍历所有记录并按组处理:
SELECT * FROM dataminer.patients ORDER BY patient_id;
在 PHP 中,是这样的:
$patientsWithRelevantAlert = array();
$currentGroupId = null;
while(... fetch into $row ...) {
$groupId = extractGroupId($row);
// Next group? Check relevant patient and reset group info.
if ($groupId != $currentGroupId) {
if (count($patientsWithRelevantAlert) == 1) {
// remember this patient
...
}
$patientsWithRelevantAlert = array();
$currentGroupId = $groupId;
}
if(hasRelevantAlert($row)) {
$patientsWithRelevantAlerts[] = $row;
}
}
// Don't forget the last group
if (count($patientsWithRelevantAlert) == 1) {
// remember this patient
...
}
应该够快了。
也就是说,SQL,数据建模和索引是为这些东西发明的。
如果这是作业:请确保您在提交时理解代码!
如果您只对只有一名成员的群组感兴趣,为什么不只select 只对计数 (patient_id) = 1 的群组感兴趣?
SELECT g.group_id, MAX(g.patient_id) FROM
( SELECT
SUBSTRING(patient_id, 1, CHAR_LENGTH(patient_id) - 1) AS group_id,
patient_id,
FROM dataminer.patients
WHERE ...
)
GROUP BY group_id
HAVING COUNT(patient_id) = 1
我找到了一个足够有效的解决方案如下:
SELECT p.patient_id, name_first, name_last, alerts
FROM patients p
JOIN (SELECT DISTINCT LEFT(patient_id, 6) AS group_id
FROM patients
WHERE patient_id BETWEEN 1000000 AND 7999999
AND alerts REGEXP '[!@#%^&]') g
ON LEFT(p.patient_id, 6) = g.group_id /* HERE is the simple magic */
ORDER BY p.patient_id
LIMIT 30000;
记录集:80,000 ~ 结果:2188 ~ 持续时间:0.312 秒 ~ 提取:0.062 秒 ~ 总计:0.374 秒
因为我们知道合法的 patient_ids 是 7 位数字长,我们可以通过简单地使用 LEFT(patient_id, 6)
而不是效率较低的 SUBSTRING(patient_id, 1, CHAR_LENGTH(patient_id) - 1)
来确定患者的 'group_id'(这我现在明白我本来可以写成 SUBSTRING(patient_id, 1, 6)
)。无论此处使用何种方法,真正节省的是对 ON
子句的更改。与其比较 patient_id 和 LIKE CONCAT(group_id, '%')
,为什么不直接 =
比较 Table 'p' 中 patient_id 的左 6 位?
换句话说,嵌套的 select 用于查找所有唯一的 'groups',其中至少一个成员具有所需警报符号之一。主要 select 使用此 table 来确定属于这些组的所有患者。本质上,LEFT(patient_id, 6) is == 'group_id'
我们可以保留我们的索引...唯一的额外开销是每行调用一次 LEFT()。
感谢大家的帮助!
编辑:因为我将在我的 PHP 算法中使用 group_id,所以我将把它添加到 select 以高效的方式:
SELECT g.group_id, RIGHT(p.patient_id, 1) AS sub_id, name_first, name_last, alerts
FROM patients p
JOIN (SELECT DISTINCT LEFT(patient_id, 6) AS group_id
FROM patients
WHERE patient_id BETWEEN 1000000 AND 7999999
AND alerts REGEXP '[!@#%^&]') g
ON LEFT(p.patient_id, 6) = g.group_id
ORDER BY p.patient_id
LIMIT 30000;
HERE is a fiddle!~注意:这不是包含的解决方案的准确表示table只有28条记录。在更大的数据集上查看以上结果。
AND finally,我用来完成处理的 PHP 算法 ~ shoutout to @The Nail:
$cur_group_id = 0;
$members = [];
$symbol = '';
$errs = false;
while($row = $result->fetch_assoc()){
$row['alerts'] = preg_replace('/[^!@#%^&]+/i', '', $row['alerts']);
if($row['group_id'] != $cur_group_id){
if($errs){
foreach($members as $member => $data){
printf('<tr><td>%d%d</td><td>%s</td><td>%s</td><td>%s</td></tr>',
$data['group_id'],
$data['sub_id'],
$data['name_last'],
$data['name_first'],
$data['alerts']);
}
}
/* reset current group */
$cur_group_id = $row['group_id'];
$members = array();
$symbol = $row['alerts'];
$errs = false;
}
$members[] = $row;
if($row['alerts'] != $symbol || strlen($row['alerts']) > 1){
$errs = true;
}
}
总处理时间(包括查询):.6 秒!!