TLDR:我需要在Web应用程序的大型数据集上进行多次中值聚合,但性能很差。我的查询可以改进/有没有比此项用例的AWS Redshift更好的数据库?
我正在开展一个团队项目,该项目涉及通过我们的网络应用程序进行可视化的大型数据集的按需聚合。我们正在使用加载了近1,000,000,000行的Amazon Redshift,按日期分配密钥(我们有2014年至今的数据,每天摄取900,000个数据点)和按唯一ID排序密钥。唯一ID与其他唯一ID可能存在一对多的关系,“很多”关系可以被认为是id的“子”。
由于机密性,请考虑这样的表结构
TABLE NAME: meal_nutrition
DISTKEY(date),
SORTKEY(patient_id),
patient_name varchar,
calories integer,
fat integer,
carbohydrates integer,
protein integer,
cholesterol integer,
sodium integer,
calories integer
TABLE NAME: patient_hierarchy
DISTKEY(date date),
SORTKEY(patient_id integer),
parent_id integer,
child_id integer,
distance integer
可以把它想象成一个有医生等级的世界。患者被封装为实际患者和医生本身,医生可以将其作为其他医生的患者。医生可以随时转移患者/医生的所有权,因此等级不断变化。
DOCTOR (id: 1)
/ \
PATIENT(id: 2) DOCTOR (id: 3)
/ \ \
P (id: 4) D (id: 8) D(id: 20)
/ \ / \ / \ \
................
我们遇到问题的一个可视化(由于性能)是一个时间序列图,显示了默认日期范围必须为1年的多个指标的日常中位数。因此,在这个例子中,我们想要患者/医生及其“孩子”所消耗的所有膳食的脂肪,碳水化合物和蛋白质的中位数,给予患者_id。使用的查询将是:
SELECT patient_name,
date,
max(median_fats),
max(median_carbs),
max(median_proteins)
FROM (SELECT mn.date date,
ph.patient_name patient_name,
MEDIAN(fats) over (PARTITION BY date) AS median_fats,
MEDIAN(carbohydrates) over (PARTITION BY date) AS median_carbs,
MEDIAN(proteins) over (PARTITION BY date) AS median_proteins
FROM meal_nutrition mn
JOIN patient_hierarchy ph
ON (mn.patient_id = ph.child_id)
WHERE ph.date = (SELECT max(date) FROM patient_hierarchy)
AND ph.parent_id = ?
AND date >= '2016-12-17' and date <= '2017-12-17'
)
GROUP BY date, patient_name
此查询中最重的操作是每个中位数的排序(每个中间需要排序〜200,000,000行),但我们无法避免这种情况。因此,此查询需要大约30秒才能完成,这意味着糟糕的用户体验。我正在进行的查询可以改进吗?这种用例有更好的数据库吗?谢谢!
如评论中所述,数据的排序/分发非常重要。如果只获得患者层次结构的一个日期切片,则您使用的所有数据都在一个节点上,并按日期分发。最好通过meal_nutrition.patient_id
和patient_hierarchy.child_id
进行分发,以便连接的数据可能分别位于同一节点上,并分别按date,patient_id
和date,child_id
对表进行排序,这样您就可以有效地找到必要的日期切片/范围,然后有效地查找患者。
至于查询本身,您可以尝试一些选项:
1)近似中位数如下:
SELECT mn.date date,
ph.patient_name patient_name,
APPROXIMATE PERCENTILE_DISC (0.5) WITHIN GROUP (ORDER BY fats) AS median_fats
FROM meal_nutrition mn
JOIN patient_hierarchy ph
ON (mn.patient_id = ph.child_id)
WHERE ph.date = (SELECT max(date) FROM patient_hierarchy)
AND ph.parent_id = ?
AND date >= '2016-12-17' and date <= '2017-12-17'
GROUP BY 1,2
注意:如果超出内存堆栈,这可能不起作用。此外,每个子查询只需要一个这样的函数,因此你不能在同一个子查询中获取脂肪,碳水化合物和蛋白质,但你可以单独计算它们然后加入。如果这样可行,则可以通过运行30s语句测试几个ID并比较结果来测试准确性。
2)分档。首先按每个值分组,或者设置合理的分箱,然后找到分布中间的组/分箱。这将是你的中位数。一个变量示例是:
WITH
groups as (
SELECT mn.date date,
ph.patient_name patient_name,
fats,
count(1)
FROM meal_nutrition mn
JOIN patient_hierarchy ph
ON (mn.patient_id = ph.child_id)
WHERE ph.date = (SELECT max(date) FROM patient_hierarchy)
AND ph.parent_id = ?
AND date >= '2016-12-17' and date <= '2017-12-17'
GROUP BY 1,2,3
)
,running_groups as (
SELECT *
,sum(count) over (partition by date, patient_name order by fats rows between unlimited preceding and current row) as running_total
,sum(count) (partition by date, patient_name) as total
FROM groups
)
,distance_from_median as (
SELECT *
,row_number() over (partition by date, patient_name order by abs(0.5-(1.0*running_total/total))) as distance_from_median
FROM running_groups
)
SELECT
date,
patient_name,
fats
WHERE distance_from_median=1
这可能允许在每个单独节点上进行分组值,并且随后使用分箱的操作将更轻,并且避免对原始集进行分类。同样,你必须进行基准测试。您拥有的独特值越低,您的性能提升就越高,因为您将从大量原始值中获得少量的分类,并且分类将更便宜。结果是准确的,除了具有偶数个不同值的选项(对于1,2,3,4
,它将返回2,而不是2.5)但是如果它是关键的,则可以通过添加另一个层来解决这个问题。主要问题是该方法本身是否显着提高了性能。
3)实现每个日期/患者id的计算。如果您唯一的参数是耐心的并且您总是计算去年的中位数,那么您可以在一夜之间将查询运行到摘要表中并查询该查询。即使(1)或(2)有助于优化性能,也会更好。您还可以在实现后将摘要表复制到Postgres实例并将其用作应用程序的后端,您将获得更好的ping(Redshift适用于实现大量数据但不如Web应用程序后端)。它带来了维护数据传输工作的成本,因此如果实现/优化做得足够好,您可以将其保留在Redshift中。
如果您尝试任何建议的选项,我真的很想获得反馈,这是Redshift的一个很好的用例。