1

背景:

首先,我有一个模式,其中有一个名为patients我关心的表,patient_id并且alerts(警报是一串字符,其中每个字符代表一些任意值/含义)。其次,每个“患者”都是一个组 [family] 的一部分,仅基于他们的 patient_id 上的前 6 位数字。此外,还有一些第三方依赖此数据库;我没有设计,也无法更改此架构/数据模型,也无法从 MySQL 迁移。

这是数据模型的小提琴


挑战:

现在,我需要找出患者有包含!@#%^&符号的警报而他们的家庭成员没有的事件。我的第一个想法是收集所有具有包含这些符号的警报的患者,删除每个患者 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 不是问题的准确表示,因为包含的表只有 28 条记录。

记录集:80,000 ~ 结果:2188 ~ 持续时间:14.321 秒 ~ 获取:0.00 秒 ~ 总计:14.321 秒


查询 #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 不是问题的准确表示,因为包含的表只有 28 条记录。

记录集:80,000 ~ 结果:2188 ~ 持续时间:4.259 秒 ~ 获取:5.663 秒 ~ 总计:9.992 秒

编辑:添加 name_first、name_last、alerts 和 order by 子句后,我发现此查询与第一个查询所用的时间完全相同。


问题:

我得到的列表准确的,但是不仅需要额外的处理(我打算用 PHP 来做),而且需要 14 秒!

如果有人有更好的......或者至少可以指出一个更好、更有效的解决方案的方向,请赐教。提前致谢。

额外的信用:任何关于 PHP 算法的技巧,以解决给定数据的上述问题 - 忘记语义,只需一个公式即可。

4

3 回答 3

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 秒

由于我们知道合法的患者 ID 是 7 位数长,我们可以通过简单地使用LEFT(patient_id, 6)而不是效率较低的SUBSTRING(patient_id, 1, CHAR_LENGTH(patient_id) - 1)(我现在看到我最初可以写为SUBSTRING(patient_id, 1, 6))来确定患者的“组 ID”。不管这里使用哪种方法,真正的节省是对ON子句的更改。与其将 patient_id 与 进行比较LIKE CONCAT(group_id, '%'),不如直接=与 Table 'p' 中的 patient_id 的左 6 位数字进行比较?

换句话说,嵌套选择用于查找所有唯一的“组”,其中至少一个成员具有所需的警报符号之一。主选择使用此表来确定属于这些组的所有患者。本质上,LEFT(patient_id, 6) is == 'group_id'我们可以保留索引……唯一的额外开销是每行调用一次 LEFT()。

“KISS”的又一个例子。

感谢大家的帮助!

编辑:由于我将在我的 PHP 算法中使用 group_id,因此我将以有效的方式将其添加到选择中:

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;

这是一个小提琴!~注意这不是解决方案的准确表示,因为包含的表只有 28 条记录。在更大的数据集上查看上述结果。


最后,我用来完成处理的 PHP 算法 ~ 向@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;
    }
}

总处理时间(包括查询):0.6 秒!!

于 2017-09-15T18:13:17.073 回答
0

如果您打算在 PHP 中进行处理,并且患者数量为 30k,我将选择按患者 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、数据建模和索引。

如果这是家庭作业:确保您在提交代码时理解代码!

于 2017-09-15T08:08:18.450 回答
0

如果您只对只有一个成员的组感兴趣,为什么不只选择 count(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
于 2017-09-15T08:24:31.863 回答