如何按三分之一或四分之一对数据进行分类 - Oracle SQL
How to bin data by thirds or quarters - Oracle SQL
我正在使用如下所示的数据集:
| Part | OpenDays |
|:-----:|:--------:|
| ABB52 | 203 |
| ABB85 | 52 |
| ABB88 | 365 |
| ABB26 | 311 |
| ABB75 | 288 |
| ABB92 | 98 |
| ABB36 | 113 |
| ABB37 | 77 |
| ABB73 | 297 |
理想情况下,我正在尝试将这些数据分组到 bin 中(分为前 3 个百分位、中间 3 个百分位或后 3 个百分位)。可能我们最终会搬到宿舍。
我正在尝试使用 OracleSQL 中的 PERCENT_RANK
函数,但不知道如何正确使用它。
我正在尝试如下操作:
SELECT
tblSO.Part,
CASE
WHEN OpenDays <= PERCENT_RANK(0.33) WITHIN GROUP (ORDER BY OpenDays DESC) THEN 1
ELSE NULL END AS OpenDaysGroup
FROM
tblSO
GROUP BY
tblSO.Part
但是得到错误:
ORA-00979: not a GROUP BY expression
理想情况下,我的数据集应该是这样的:
| Part | OpenDays |
|-------|----------|
| ABB88 | 3 |
| ABB26 | 3 |
| ABB73 | 3 |
| ABB75 | 2 |
| ABB52 | 2 |
| ABB36 | 2 |
| ABB92 | 1 |
| ABB37 | 1 |
| ABB85 | 1 |
我已设置此 SQLFIDDLE 以使用示例数据进行演示。
如何使用 PERCENT_RANK
来实现我的目标?
老实说,我通常为此使用蛮力:
SELECT tblSO.Part,
FLOOR( (ROW_NUMBER() OVER (ORDER BY OpenDays DESC) - 1) * 3 /
COUNT(*) OVER ()
) AS OpenDaysGroup
FROM tblSO;
为什么?首先,计算不是很复杂。更重要的是,谁能记得内置函数是如何处理关系的。例如 NTILE()
——它也做你想做的——将领带放在不同的组中。上面的代码也是如此。
但是,如果我想在同一组中有关系,我会使用 RANK()
而不是 ROW_NUMBER()
:
SELECT tblSO.Part,
FLOOR( (RANK() OVER (ORDER BY OpenDays DESC) - 1) * 3 /
COUNT(*) OVER ()
) AS OpenDaysGroup
FROM tblSO;
Here 是一个 SQLFiddle。
我问了 OP 以下问题:查询的主要 objective 是什么?将 "parts" 分配给箱子,这样每个箱子都包含相同数量的 部分 ,尽可能多 - 无论 open 的总数如何每个垃圾箱中的天数?还是尽可能地平衡各个箱子的 开放天数 总数,即使这意味着一些箱子将接收更多零件(但每个箱子的开放天数较少) ?显然,这两个 objective 是相互矛盾的;这两个问题是不同的。
OP 尚未回复。但后一个问题很有趣,值得以任何一种方式解决。
编辑 我的问题(作为评论发布在原始问题下)似乎已被删除。谁做的没有留下任何解释。无论如何,问题已在上面转载。 END EDIT
Gordon Linoff 提供了一个解决方案,可以使每个箱子中的零件数量相等;在那个解决方案(和问题陈述,真的)中,输入中每一行的 "open days" 计数完全没有作用。这是(两种可能解释中的)微不足道的问题。
这个难题 - 尽可能均衡每个箱子中的开放天数总和 - 可以通过完整枚举来解决(考虑所有可能的分配,选择最好的),但这很快变得不切实际。一个人需要一个算法,一个人也可能需要接受一个 "good enough" 的解决方案,同时它相当好而且实用(查询应该在不到一年的时间内完成,比如说,对于中等大的输入数据集)。
下面是这样的算法,然后是Oracle中的实现SQL——使用递归WITH子句,需要Oracle 11.2或更高版本。
从三个箱子开始,按 "open days" 降序排列 "parts"。以任何方式通过 "open days" 打破平局 - 这并不重要,但必须打破平局。在初步子查询中使用解析 ROW_NUMBER()
函数很容易。
跟踪到目前为止每个垃圾箱中的总开放天数。 (在我们开始之前,每个 bin 中的总数为零。)在每个步骤中,取出下一部分(按 ROW_NUMBER()
排序)并将其分配给当前开放天数总和最少的 bin。如果有联系,优先选择 bin 1,然后是 bin 2,而不是其他 bin。
这是查询和结果 - 在 CTE 中(在 WITH 子句中)输入数据用于测试。
with
tblso (part, opendays) as (
select 'ABB52', 203 from dual union all
select 'ABB85', 52 from dual union all
select 'ABB88', 365 from dual union all
select 'ABB26', 311 from dual union all
select 'ABB75', 288 from dual union all
select 'ABB92', 98 from dual union all
select 'ABB36', 113 from dual union all
select 'ABB37', 77 from dual union all
select 'ABB73', 297 from dual
)
, prep (part, opendays, rn) as (
select part, opendays, row_number() over (order by opendays desc) from tblso
)
, r (part, opendays, rn, bin, sum1, sum2, sum3) as (
select part, opendays, 1, 1, opendays, 0, 0
from prep
where rn = 1
union all
select p.part, p.opendays, p.rn,
case when r.sum1 <= r.sum2 and r.sum1 <= r.sum3 then 1
when r.sum2 <= r.sum3 then 2
else 3 end,
case when r.sum1 <= r.sum2 and r.sum1 <= r.sum3 then r.sum1 + p.opendays
else r.sum1 end,
case when r.sum2 < r.sum1 and r.sum2 <= r.sum3 then r.sum2 + p.opendays
else r.sum2 end,
case when r.sum3 < r.sum1 and r.sum3 < r.sum2 then r.sum3 + p.opendays
else r.sum3 end
from prep p join r on p.rn = r.rn + 1
)
select part, opendays, bin
from r;
输出:
PART OPENDAYS BIN
----- ---------- ----------
ABB88 365 1
ABB26 311 2
ABB73 297 3
ABB75 288 3
ABB52 203 2
ABB36 113 1
ABB92 98 1
ABB37 77 2
ABB85 52 1
为了比较,这里是 Gordon 答案中的 "part" 计数和 "open days" 总和,与此答案中的查询:
总帐摘要
BIN PART_COUNT TOTAL_OPENDAYS
---------- ---------- --------------
0 3 973
1 3 604
2 3 227
数学家总结
BIN PART_COUNT TOTAL_OPENDAYS
---------- ---------- --------------
1 4 628
2 3 591
3 2 585
Gordon 的解决方案将九个输入部分中的三个分配给三个箱子中的每一个,但每个箱子的总开放天数在 227 到 973 之间。在我的解决方案中,一个箱子有四个部分,另一个箱子只有两个;但每个箱子的总开放天数从 585 天到 628 天不等 - 分布更紧凑。
虽然不能保证,一般来说,我在这里描述的算法会找到最优解(要明确:hard 问题),但几乎可以肯定的是在几乎所有情况下,它都会找到比为每个 bin 分配相同数量的 "parts" 更好的解决方案。
我正在使用如下所示的数据集:
| Part | OpenDays |
|:-----:|:--------:|
| ABB52 | 203 |
| ABB85 | 52 |
| ABB88 | 365 |
| ABB26 | 311 |
| ABB75 | 288 |
| ABB92 | 98 |
| ABB36 | 113 |
| ABB37 | 77 |
| ABB73 | 297 |
理想情况下,我正在尝试将这些数据分组到 bin 中(分为前 3 个百分位、中间 3 个百分位或后 3 个百分位)。可能我们最终会搬到宿舍。
我正在尝试使用 OracleSQL 中的 PERCENT_RANK
函数,但不知道如何正确使用它。
我正在尝试如下操作:
SELECT
tblSO.Part,
CASE
WHEN OpenDays <= PERCENT_RANK(0.33) WITHIN GROUP (ORDER BY OpenDays DESC) THEN 1
ELSE NULL END AS OpenDaysGroup
FROM
tblSO
GROUP BY
tblSO.Part
但是得到错误:
ORA-00979: not a GROUP BY expression
理想情况下,我的数据集应该是这样的:
| Part | OpenDays |
|-------|----------|
| ABB88 | 3 |
| ABB26 | 3 |
| ABB73 | 3 |
| ABB75 | 2 |
| ABB52 | 2 |
| ABB36 | 2 |
| ABB92 | 1 |
| ABB37 | 1 |
| ABB85 | 1 |
我已设置此 SQLFIDDLE 以使用示例数据进行演示。
如何使用 PERCENT_RANK
来实现我的目标?
老实说,我通常为此使用蛮力:
SELECT tblSO.Part,
FLOOR( (ROW_NUMBER() OVER (ORDER BY OpenDays DESC) - 1) * 3 /
COUNT(*) OVER ()
) AS OpenDaysGroup
FROM tblSO;
为什么?首先,计算不是很复杂。更重要的是,谁能记得内置函数是如何处理关系的。例如 NTILE()
——它也做你想做的——将领带放在不同的组中。上面的代码也是如此。
但是,如果我想在同一组中有关系,我会使用 RANK()
而不是 ROW_NUMBER()
:
SELECT tblSO.Part,
FLOOR( (RANK() OVER (ORDER BY OpenDays DESC) - 1) * 3 /
COUNT(*) OVER ()
) AS OpenDaysGroup
FROM tblSO;
Here 是一个 SQLFiddle。
我问了 OP 以下问题:查询的主要 objective 是什么?将 "parts" 分配给箱子,这样每个箱子都包含相同数量的 部分 ,尽可能多 - 无论 open 的总数如何每个垃圾箱中的天数?还是尽可能地平衡各个箱子的 开放天数 总数,即使这意味着一些箱子将接收更多零件(但每个箱子的开放天数较少) ?显然,这两个 objective 是相互矛盾的;这两个问题是不同的。
OP 尚未回复。但后一个问题很有趣,值得以任何一种方式解决。
编辑 我的问题(作为评论发布在原始问题下)似乎已被删除。谁做的没有留下任何解释。无论如何,问题已在上面转载。 END EDIT
Gordon Linoff 提供了一个解决方案,可以使每个箱子中的零件数量相等;在那个解决方案(和问题陈述,真的)中,输入中每一行的 "open days" 计数完全没有作用。这是(两种可能解释中的)微不足道的问题。
这个难题 - 尽可能均衡每个箱子中的开放天数总和 - 可以通过完整枚举来解决(考虑所有可能的分配,选择最好的),但这很快变得不切实际。一个人需要一个算法,一个人也可能需要接受一个 "good enough" 的解决方案,同时它相当好而且实用(查询应该在不到一年的时间内完成,比如说,对于中等大的输入数据集)。
下面是这样的算法,然后是Oracle中的实现SQL——使用递归WITH子句,需要Oracle 11.2或更高版本。
从三个箱子开始,按 "open days" 降序排列 "parts"。以任何方式通过 "open days" 打破平局 - 这并不重要,但必须打破平局。在初步子查询中使用解析 ROW_NUMBER()
函数很容易。
跟踪到目前为止每个垃圾箱中的总开放天数。 (在我们开始之前,每个 bin 中的总数为零。)在每个步骤中,取出下一部分(按 ROW_NUMBER()
排序)并将其分配给当前开放天数总和最少的 bin。如果有联系,优先选择 bin 1,然后是 bin 2,而不是其他 bin。
这是查询和结果 - 在 CTE 中(在 WITH 子句中)输入数据用于测试。
with
tblso (part, opendays) as (
select 'ABB52', 203 from dual union all
select 'ABB85', 52 from dual union all
select 'ABB88', 365 from dual union all
select 'ABB26', 311 from dual union all
select 'ABB75', 288 from dual union all
select 'ABB92', 98 from dual union all
select 'ABB36', 113 from dual union all
select 'ABB37', 77 from dual union all
select 'ABB73', 297 from dual
)
, prep (part, opendays, rn) as (
select part, opendays, row_number() over (order by opendays desc) from tblso
)
, r (part, opendays, rn, bin, sum1, sum2, sum3) as (
select part, opendays, 1, 1, opendays, 0, 0
from prep
where rn = 1
union all
select p.part, p.opendays, p.rn,
case when r.sum1 <= r.sum2 and r.sum1 <= r.sum3 then 1
when r.sum2 <= r.sum3 then 2
else 3 end,
case when r.sum1 <= r.sum2 and r.sum1 <= r.sum3 then r.sum1 + p.opendays
else r.sum1 end,
case when r.sum2 < r.sum1 and r.sum2 <= r.sum3 then r.sum2 + p.opendays
else r.sum2 end,
case when r.sum3 < r.sum1 and r.sum3 < r.sum2 then r.sum3 + p.opendays
else r.sum3 end
from prep p join r on p.rn = r.rn + 1
)
select part, opendays, bin
from r;
输出:
PART OPENDAYS BIN
----- ---------- ----------
ABB88 365 1
ABB26 311 2
ABB73 297 3
ABB75 288 3
ABB52 203 2
ABB36 113 1
ABB92 98 1
ABB37 77 2
ABB85 52 1
为了比较,这里是 Gordon 答案中的 "part" 计数和 "open days" 总和,与此答案中的查询:
总帐摘要
BIN PART_COUNT TOTAL_OPENDAYS
---------- ---------- --------------
0 3 973
1 3 604
2 3 227
数学家总结
BIN PART_COUNT TOTAL_OPENDAYS
---------- ---------- --------------
1 4 628
2 3 591
3 2 585
Gordon 的解决方案将九个输入部分中的三个分配给三个箱子中的每一个,但每个箱子的总开放天数在 227 到 973 之间。在我的解决方案中,一个箱子有四个部分,另一个箱子只有两个;但每个箱子的总开放天数从 585 天到 628 天不等 - 分布更紧凑。
虽然不能保证,一般来说,我在这里描述的算法会找到最优解(要明确:hard 问题),但几乎可以肯定的是在几乎所有情况下,它都会找到比为每个 bin 分配相同数量的 "parts" 更好的解决方案。