算法或 SQL:查找一组列的条件,确保结果集在特定列中的值始终 > 0

Posted

技术标签:

【中文标题】算法或 SQL:查找一组列的条件,确保结果集在特定列中的值始终 > 0【英文标题】:Algorithm or SQL : to find where conditions for a set of columns which ensures result set has value in a particular column always > 0 【发布时间】:2015-01-29 06:46:14 【问题描述】:

我正在开发一个基于 java-oracle 的项目,我遇到了一个在我看来需要分析解决方案的问题。 我正在寻找基于 SQL 查询或任何算法或任何免费分析工具的解决方案,我可以遵循这些工具以获得所需的结果。

问题陈述: 假设我有下表,列 A-D 和最后一列作为分数,我想找到每个列的值的标准,当在 SQL where 子句中组合时,总能给我的分数列提供正值。那么基本上A-D列的哪种组合总是会给我积极的分数?

columnA|columnB|columnC|columnD|Score
  1      40      10       3     -20
  0      40      2        3      10
  0      10      3        3      20
  1      15      3        3     -5
  0      10      2        2     -15
  0      15      6        3     -10

上述数据集的预期结果:- 对上述数据集的视觉解释给了我条件:“ColumnA =0 and columnB >10 and columnC 0”。 (视觉上它清晰的 columnD 没有效果)。

请注意以上数据集是为了简单起见。实际上,我的项目包含大约 40 列,近 2500 行。有一件事是确定每一列都有有限的值范围。


从下面的 OP 答案中复制的以下信息

这是我开始使用的算法(如果有人认为我的方向正确,需要输入来进一步完善它):

准备工作:创建一个包含所有可能表达式的列表,例如 A=0、B>10、C

我们称之为“表达式”变量。

第一次运行的算法:

    set totalPositiveRows= select count(*) from my tables where score>0;

    set totalNegativeRows= select count(*) from my tables where score

    对于表达式中的每个 expr,计算以下三个变量 set positivePercentage= 找到满足这个 expr 的 totalPositiveRows 的百分比; //就像如果总共 100 行中有 60 行的 score>0 满足 expr ,那么 positivePercentage=60%

    set negativePercentage= find percentage of totalNegativeRows which satisfy this expr; //like if 40 rows out of total 100 rows  having score<0 satisfy expr , then negativePercentage=40%
    
    set diffPercentage=positivePercentage-negativePercentage;
    

    Set initialexpr=选择具有最大值 diffPercentage 的 expr set initalPositivePercentage=选择对应的正百分比值; set initalNegativePercentage=选择对应的负百分比值; 我的想法是我现在需要继续扩展 initalexpr 直到 initalNegativePercentage 变为 0。

后续运行的算法,直到 initalNegativePercentage 变为 0:-

    对于表达式中的每个 expr,计算以下三个变量 设置 newexpr=initialexpr+" 和 "+expr; set positivePercentage= 找到满足 newexpr 的 totalPositiveRows 的百分比; setnegativePercentage= 找到满足 newexpr 的 totalNegativeRows 的百分比;

    //计算它减少了多少负百分比? 设置 positiveReduction=initialPositivePercentage-positivePercentage; 设置negativeReduction=initialNegativePercentage-negativePercentage; 如果(负还原>=正还原) //记下来 别的 //丢弃

    选择给出最大负减少的 expr,这将成为新的初始 expr。 设置 initialexpr=选择 expr 的最大值为negativeReduction set initalPositivePercentage=选择对应的值; set initalNegativePercentage=选择对应的值;

    重复上述算法。

请发表评论。

【问题讨论】:

其他 2500 行对您的“上述数据集的预期结果”有何看法?你的说法是否也是如此? @PresidentCamacho ,是的,正分数是所有列的逻辑表示,它适用于所有 2500 行。我知道应该有可行的解决方案,我只是不知道如何得到它。 使用 CASE 构造,看我的回答。 啊,现在我明白了。分数不是派生的,但您想要当前记录集的“某种派生”。因此,当添加新行时,您必须重新计算该推导。 一个简单的解决方案是选择任何具有正分数的行,然后通过要求列的值等于其在该行中的值来形成表达式(例如A=0, B=50, C=1, D=3)。如果这不是您想要的,那么您可能想要的是返回 最大可能 行集的表达式。但我很确定,由于交互作用,有多种不同的表达方式可以做到这一点。 【参考方案1】:

下面是一个蛮力解决方案。对于理论计算机科学网站来说,这也可能是一个很好的问题。我认为这是一个类似于Boolean satisfiability 的 NP 完全问题,但这只是一个疯狂的猜测。可能有更聪明的方法来解决这个问题,但我想我不会找到它。

基本思想是将每个表达式与列的每个不同值交叉连接,然后交叉连接所有列。将使用每个表达式列表查询该表,并为正负分生成计数。如果表达式返回预期的正分数数并且没有负分数,则它是有效的。

这假设您只使用表达式&gt;&lt;=。每一个新的列或表达式都会使这个问题成倍地变慢。

测试架构

drop table table1;
create table table1(a number, b number, c number, d number, score number);

insert into table1
select  1,      40,      10,       3,     -20 from dual union all
select  0,      40,      2,        3,      10 from dual union all
select  0,      10,      3,        3,      20 from dual union all
select  1,      15,      3,        3,     -5  from dual union all
select  0,      10,      2,        2,     -15 from dual union all
select  0,      15,      6,        3,     -10 from dual;

代码墙

declare
    v_inline_view varchar2(32767);
    v_inline_views clob;
    v_inline_view_counter number := 0;
    v_and_expression varchar2(32767);
    v_query clob;

    v_sqls sys.odcivarchar2list;
    v_dynamic_query_counter number := 0;
begin
    --#1: Create inline views.
    --One for every combination of expression and distinct value, per column.
    for inline_views in
    (
        --Inline view for every possible expression for each column.
        select
            replace(q'[
            (
                select *
                from
                (
                    --Possible expressions.
                    select distinct
                        case
                            when operator is null then null
                            else ' AND %%COLUMN%% '||operator||' '||%%COLUMN%%
                        end %%COLUMN%%_expression
                    from
                    --All operators.
                    (
                        select '>'  operator from dual union all
                        select '<'  operator from dual union all
                        select '='  operator from dual union all
                        select null operator from dual
                    )
                    --All distinct values.
                    cross join
                    (
                        select distinct %%COLUMN%% from table1
                    )
                )
                --where %%COLUMN%%_expression is null or %%COLUMN%%_expression %%EXPRESSION_PERFORMANCE_EXCLUSIONS%%
            )
            ]', '%%COLUMN%%', column_name) inline_view
        from user_tab_columns
        where table_name = 'TABLE1'
            and column_name <> 'SCORE'
        order by column_name
    ) loop
        --Assign to temorary so it can be modified.
        v_inline_view := inline_views.inline_view;

        --#1A: Optimize inline view - throw out expressions if they don't return any positive results.
        declare
            v_expressions sys.odcivarchar2list;
            v_expressions_to_ignore varchar2(32767);
            v_has_score_gt_0 number;
        begin
            --Gather expressions for one column.
            execute immediate v_inline_view bulk collect into v_expressions;

            --Loop through and test each expression.
            for i in 1 .. v_expressions.count loop
                --Always keep the null expression.
                if v_expressions(i) is not null then
                    --Count the number of rows with a positive score.
                    execute immediate 'select nvl(max(case when score > 0 then 1 else 0 end), 0) from table1 where '||replace(v_expressions(i), ' AND ', null)
                    into v_has_score_gt_0;

                    --If the expression returns nothing positive then add it to exclusion.
                    if v_has_score_gt_0 = 0 then
                        v_expressions_to_ignore := v_expressions_to_ignore||','''||v_expressions(i)||'''';
                    end if;
                end if;
            end loop;

            --Convert it into an IN clause.
            if v_expressions_to_ignore is not null then
                --Remove comment, replace placeholder with expression exclusions.
                v_inline_view := regexp_replace(v_inline_view, '(.*)(--where)( .* )(%%EXPRESSION_PERFORMANCE_EXCLUSIONS%%)(.*)', '\1where\3 not in ('||substr(v_expressions_to_ignore, 2)||')');
            end if;
        end;

        --Aggregate and count inline views.
        if v_inline_view_counter <> 0 then
            v_inline_views := v_inline_views||'cross join';
        end if;

        v_inline_views := v_inline_views||v_inline_view;

        v_inline_view_counter := v_inline_view_counter + 1;
    end loop;

    --#2: Create an AND expression to combine all column expressions.
    select listagg(column_name||'_expression', '||') within group (order by column_name)
    into v_and_expression
    from user_tab_columns
    where table_name = 'TABLE1'
        and column_name <> 'SCORE';


    --#3: Create a that will create all possible expression combinations.
    v_query := 
    replace(replace(q'[
        --8281 combinations
        select '
            select
                '''||expressions||''' expression,
                nvl(sum(case when score > 0 then 1 else 0 end), 0) gt_0_score_count,
                nvl(sum(case when score <= 0 then 1 else 0 end), 0) le_0_score_count
            from table1
            where 1=1 '||expressions v_sql
        from
        (
            --Combine expressions
            select %%AND_EXPRESSION%% expressions
            from
            %%INLINE_VIEWS%%
        ) combined_expressions
    ]', '%%AND_EXPRESSION%%', v_and_expression), '%%INLINE_VIEWS%%', v_inline_views);

    --TEST: It might be useful to see the full query here.
    --dbms_output.put_line(v_query);

    --#4: Gather expressions.
    --With larger input you'll want to use a LIMIT
    execute immediate v_query
    bulk collect into v_sqls;

    --#5: Test each expression.  
    --Look for any queries that return the right number of rows.
    for i in 1 .. v_sqls.count loop
        declare
            v_expression varchar2(4000);
            v_gt_0_score_count number;
            v_le_0_score_count number;
        begin
            execute immediate v_sqls(i) into v_expression, v_gt_0_score_count, v_le_0_score_count;
            v_dynamic_query_counter := v_dynamic_query_counter + 1;

            --TODO: Dynamically generate "2".
            if v_gt_0_score_count = 2 and v_le_0_score_count = 0 then
                dbms_output.put_line('Expression: '||v_expression);
            end if;

        exception when others then
            dbms_output.put_line('Problem with: '||v_sqls(i));
        end;
    end loop;

    dbms_output.put_line('Queries executed: '||v_dynamic_query_counter);

end;
/

结果

结果显示正确。它们与您的略有不同,因为“columnB > 10”不正确。

Expression:  AND A = 0 AND C < 6 AND D = 3
Expression:  AND A = 0 AND C < 6 AND D > 2
Expression:  AND A < 1 AND C < 6 AND D = 3
Expression:  AND A < 1 AND C < 6 AND D > 2
Queries executed: 441

问题

这种蛮力方法至少在两个方面效率极低。即使对于这个简单的示例,它也需要 6370 次查询。结果可能包括重复性,这些重复性对于减少是非常重要的。或者,也许您会很幸运,并且您可以关注它们的解决方案太少了。

您可以采取一些措施来提高查询性能。最简单的方法是单独检查每个条件,如果计数没有“获得”任何东西,则将其丢弃。

优化

不返回任何肯定结果的单个表达式被排除在外。使用示例数据,这将查询执行次数从 6370 减少到 441。

并行运行进程也可以将性能提高一个数量级。它可能需要一个并行流水线函数。

但即使是 100 倍的性能提升也可能无助于解决 NP 完全问题。您可能需要根据您的样本数据找到一些额外的“捷径”。

取消注释dbms_output.put_line 语句之一可能有助于打印出生成测试查询的查询。添加count(*) 以查看将执行多少查询并使用较小的数据集运行以估计需要多长时间。

如果估计是 10 亿年,而你想不出任何技巧来让蛮力方法更快地工作,也许是时候在 https://cstheory.stackexchange.com/ 上提问这个问题了

【讨论】:

感谢乔恩·海勒。这看起来不错,我需要在我更大的数据集上尝试这个概念......会更新你。 当我为大型数据集尝试 20 列时,我的系统刚刚挂起......正如你所期望的那样,所花费的时间呈指数级增长...... 感谢您所做的努力...我几乎花了整整 2 天的时间来使您的逻辑在我的数据集上运行..不幸的是,结果徒劳无功....我已经开辟了一个期待更多的赏金上述问题的算法,否则我想我需要在 cstheory 中发布它。 能否请您检查一下我发布的算法并提供您的意见? @ag112 我很难理解所有这些算法。您能否用更多的行和列扩展该示例,并修复“columnB > 10”问题?我想我可能有更好的方法来做到这一点,但我需要更多的数据来测试它是否更有效。【参考方案2】:

解决方案的想法是列是独立的。所以可以逐列求解。

所以你可以想象你在多维空间中搜索和构建一些东西。每列代表一个维度,其值从-inf+inf。并按维度构建解决方案。

对于第一列,解决方案是:A=1 =&gt; false, A=0 =&gt; true. 然后添加第二个维度 B。您有 5 个值,因此 B 列上的维度被分为 6 个区间。可以连接一些连续的间隔。例如 和 都暗示为真。 然后添加第三维。 ...

如果您想在 SQL 级别加入维度区间,您可以使用 LAG 函数。通过使用分区和窗口,您可以按一列对行进行排序。然后你在一个熟的列中计算一个值真/假。并在下一个熟列中使用 LAG 函数检测真/假标志是否与前一行相比发生了变化。

create table test
(
  b number,
  s number
);

insert into test values(10, -20);
insert into test values(50,  10);
insert into test values(15,  20);
insert into test values(18,  5);

select u.*,
 case when LAG (b, 1, null) OVER (ORDER BY b) = b then 'Y' else 'N' end same_b_value,
 LAG (score_flag, 1, null) OVER (ORDER BY b) AS score_flag_prev,
 case when LAG (score_flag, 1, null) OVER (ORDER BY b) <> score_flag then 'Y' else 'N' end score_flag_changed
from
(
  select t.*,
  case when t.s >= 0 then 'Y' else 'N' end as score_flag
  from test t
)  u
order by b asc;

此查询将显示值 B=15 很重要,因为它是 score_flag 更改的地方。

我不确定问题中的值 B=10。因为这与正负分值有关。那么应该包含还是排除呢?

【讨论】:

谢谢。您能否用更多列扩展您的代码并加入其中。我自己真的想不通............【参考方案3】:

非常有趣的问题。我的提议基于函数 check_column,代码如下。执行示例:

select CHECK_COLUMN('col01') from dual;    => "COL01 = 0"
select CHECK_COLUMN('col03') from dual;    => "COL03 <= 2"

select column_name cn, check_column(column_name) crit 
  from all_tab_columns atc      
  where atc.table_name='SCORES' and column_name like 'COL%';

cn          crit
COL01       COL01 = 0
COL02       COL02 >= 32  
COL03       COL03 <= 2
COL04       COL04 = COL04

在您的示例中,第 3 行,B 列我将值 10 替换为 32,因为示例不好,并且条件 和列 B >10 不正确。 Col04 仅用于演示,因为它是中性的。您需要在 java 或 sql 中将输出字符串粘贴在一起,但这应该不是问题。

我将基表命名为scores,然后创建了视图positives,您可以代替视图将数据放在一些临时表中,执行速度应该会快得多。

create or replace view positives as
  select distinct col01, col02, col03, col04 
    from scores where score>0
  minus select COL01,COL02,COL03,COL04
    from scores where score<0;

功能是:

create or replace function check_column(i_col in varchar2) return varchar2 as
  v_tmp number;
  v_cnt number;
  v_ret varchar2(4000);
begin
  -- candidate for neutral column ?
  execute immediate 'select count(distinct '||i_col||') from positives' into v_tmp;
  execute immediate 'select count(distinct '||i_col||') from scores' into v_cnt;
  if v_tmp = v_cnt then
    return i_col||' = '||i_col;   -- empty string is better, this is for presentation 
  end if;

  -- candidate for "column = some_value" ?
  execute immediate 'select count(distinct '||i_col||') from positives' into v_cnt;
  if v_cnt = 1 then
    execute immediate 'select distinct '||i_col||' from positives' into v_tmp;
    return i_col||' = '||v_tmp; 
  end if;

  -- is this candidate for "column >= some_value" ?
  execute immediate 'select min(distinct '||i_col||') from positives' into v_tmp;
  execute immediate 'select count(1) from scores where '||i_col||
    ' not in (select '||i_col||' from positives) and '||i_col||'>'||v_tmp into v_cnt;
  if v_cnt = 0 then
    execute immediate 'select min('||i_col||') from scores' into v_cnt;
    if v_cnt != v_tmp then 
      return i_col||' >= '||v_tmp; 
    end if;
  end if;

  -- is this candidate for "column <= some_value" ?
  execute immediate 'select max(distinct '||i_col||') from positives' into v_tmp;
  execute immediate 'select count(1) from scores where '||i_col||
    ' not in (select '||i_col||' from positives) and '||i_col||'<'||v_tmp into v_cnt;
  if v_cnt = 0 then 
    execute immediate 'select max('||i_col||') from scores' into v_cnt;
    if v_cnt != v_tmp then
      return i_col||' <= '||v_tmp;
    end if;
  end if;

  -- none of the above, have to list specific values
  execute immediate 'select listagg('||i_col||', '', '') '
    ||'within group (order by '||i_col||') '
    ||'from (select distinct '||i_col||' from positives)' into v_ret;
  return i_col||' in ('||v_ret||')';
end check_column;

此解决方案既未优化也未经过大量测试,请小心。 如果您的 Oracle 版本

【讨论】:

我需要一点时间来理解你的逻辑:) 我认为你的逻辑不会起作用,因为你的逻辑依赖于积极的观点,而我期望逻辑应该考虑消极的观点。例如您的“column = some_value”的第一个候选条件,假设我有一列的所有行的值为5(正分和负分),那么我不希望在 where 条件下考虑该列,但您的逻辑将拿起它。 更改了函数中的步骤顺序。修改视图 positives 以消除场景 (15, 5, 7, 2, -1) (15, 5, 7, 2, 1)。如果视图为空,则无法构建输出条件。【参考方案4】:

我会这样做:

    检查每个“输入”列的最小值和最大值 检查分数 > 0 的子集的每个“输入”列的最小值和最大值

现在,对于每个“输入”列:

    如果点 1 和点 2 的最小值和最大值相同,则该列没有方位 否则,如果点 2 的最小值和最大值相同,则该列是“=” 否则,如果最小值相同但最大值不同,则该列是一个“ 否则,如果最大值相同但最小值不同,则该列是“>”,以第 2 点的最小值作为参考 否则,该列是“”

请注意,这一切都假设分数(假设)由“输入”列中的连续范围驱动。它无法识别“10”或“12”等条件。由于这些都不在您的示例中,我推测它可能没问题,但如果不是,您将回到 NP-complete...

生成查询以输出任意模式的上述条件的 SQL 应该相对容易构建。让我知道你是否需要帮助,我会调查的。

【讨论】:

【参考方案5】:
SELECT min(a), max(a) from MyTable WHERE score > 0;
SELECT min(a), max(a) from MyTable;

SELECT min(b), max(b) from MyTable WHERE score > 0;
SELECT min(b), max(b) from MyTable;

SELECT min(c), max(c) from MyTable WHERE score > 0;
SELECT min(c), max(c) from MyTable;

SELECT min(d), max(d) from MyTable WHERE score > 0;    
SELECT min(d), max(d) from MyTable;

这将为您提供每列的正分数范围,然后是这些列在所有分数中的范围。当这些范围不同时,您就有相关性

【讨论】:

不,它不起作用...有很多列具有二进制数据,即只有 0 和 1...那里的最小值和最大值是什么意思? 意思和其他列完全一样;如果一列的范围为 0 - 1,并且得分 >0 范围也是 0 到 1,则不存在相关性。如果一列的范围是 0 到 1,并且得分 > 0 的范围是 0 到 0 或 1 到 1,那么您就有关联。【参考方案6】:

这是我开始使用的算法(如果有人认为我的方向正确,需要输入来进一步完善它):

准备工作: 创建一个包含所有可能表达式的列表,例如 A=0、B>10、C

我们称之为“表达式”变量。

第一次运行的算法:

1. set totalPositiveRows= select count(*) from my tables where score>0;

   set totalNegativeRows= select count(*) from my tables where score<0;

2.   For each expr in expressions, calculate following three variables
        set positivePercentage= find percentage of totalPositiveRows which satisfy this expr; //like if 60 rows out of total 100 rows  having score>0 satisfy expr , then positivePercentage=60%

        set negativePercentage= find percentage of totalNegativeRows which satisfy this expr; //like if 40 rows out of total 100 rows  having score<0 satisfy expr , then negativePercentage=40%

        set diffPercentage=positivePercentage-negativePercentage;

3. Set initialexpr=Choose expr having maximum value of diffPercentage
   set initalPositivePercentage=choose corresponding positivePercentage value; 
   set initalNegativePercentage=choose corresponding negativePercentage value;

我的想法是我现在需要继续扩展 initalexpr 直到 initalNegativePercentage 变为 0。

后续运行的算法,直到 initalNegativePercentage 变为 0:-

1. For each expr in expressions, calculate following three variables        
  set newexpr=initialexpr+" and "+expr;
  set positivePercentage= find percentage of totalPositiveRows which satisfy newexpr;
  set negativePercentage= find percentage of totalNegativeRows which satisfy  newexpr;

  //calculate how much negative percentage it has reduced?
  set positiveReduction=initalPositivePercentage-positivePercentage;
  set negativeReduction=initalNegativePercentage-negativePercentage;
  if(negativeReduction>=positiveReduction)
    //note it down
  else
    //discard it

2. Choose the expr which gives maxium negative reduction, that becomes new inital expr.
    Set initialexpr=Choose expr having maximum value of negativeReduction
   set initalPositivePercentage=choose corresponding value; 
   set initalNegativePercentage=choose corresponding value;

3. Repeat the algorithm above.   

请发表评论。

【讨论】:

所以你可以想象你在多维空间中搜索和构建一些东西。每列代表一个维度。并按维度构建解决方案。对于第一列,解决方案是:A=1 =&gt; false, A=0 =&gt; true。然后添加第二维B。您有 5 个值,因此 B 列上的维度被分为 6 个区间。可以连接一些连续的间隔。例如&lt;10, 50&gt; and &lt;50,inf&gt; 都暗示true。然后添加第三维。 PS:如果你想在 SQL 级别加入维度区间,你可以使用LAG 函数。通过使用分区和窗口,您可以按一列对行进行排序。然后你在一个熟的列中计算一个值真/假。在下一个熟列中,通过使用LAG 函数,您可以检测真/假标志是否与上一行相比发生了变化。 @ibre5041...对不起,我不是来自分析领域,所以真的不了解 LAG 函数术语等....您可以举一个工作示例吗?我擅长java和pl/sql。 见这个小提琴sqlfiddle.com/#!4/1891c/9。 select u.*, LAG (score_flag, 1, null) OVER (ORDER BY b) AS score_flag_prev, case when LAG (score_flag, 1, null) OVER (ORDER BY b) &lt;&gt; score_flag then 'Y' else 'N' end score_flag_changed from ( select t.*, case when t.s &gt;= 0 then 'Y' else 'N' end as score_flag from test t ) u order by b asc; 此查询将根据列 B 对行进行排序,列 score_flag_changed 将标记 B 中对行有效性很重要的值 @ibre5041,您能否将其详细说明为单独的答案,以便有更多的写作空间.....所以在您的示例中,对于 B=15,score_flag_changed=Y,这是否意味着 B=15选择条件的候选者之一?...您如何将这个概念进一步用于在条件之间进行“与”?【参考方案7】:

这是一个简单的实现,会产生一组复杂的规则。

令 A 为所有输入的结果为正分数的集合,B 为所有输入的结果为不为正分数的集合。

如果任何一组输入同时存在于 A 和 B 中,则没有规则会给出所有的正数,也不会给出负数。无论如何,A-B 是一组只给出正值的规则,没有任何一组排除所有非正值的规则可以做得更好。

在您的示例中,我们的规则是: (colA=0, colB=40, colC=2, colD=3), (colA=0, colB=10, colC=3, colD=3)。

【讨论】:

一旦有了这些规则,就可以寻找共性了。

以上是关于算法或 SQL:查找一组列的条件,确保结果集在特定列中的值始终 > 0的主要内容,如果未能解决你的问题,请参考以下文章

在 ORACLE SQL 中将一组列转换为行

如何应用于具有多索引列的数据框中的一组列

从数据框中删除不包括一组列的列中的nan行。

根据另一列的位置从一组列中返回值

SQL必知必会 20160926--

SQL查询 - 按另一列排序一组列