算法或 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

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

问题陈述: 让我们说我在 table 下面有 columnA-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 <5 will ensure score always >0”。 (在视觉上它的clear columnD没有作用)。

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


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

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

准备:创建所有可能的表达式列表,如 A=0、B>10、C<5(对于 40 列,我最终确定了总共约 150 个表达式)

我们称它为"expressions"变量。

第一个 运行 的算法:

  1. set totalPositiveRows= select count(*) 从我的 tables where score>0;

    设置 totalNegativeRows= select count(*) 从我的 tables where score<0;

  2. 对于表达式中的每个expr,计算以下三个变量 设置 positivePercentage= 找到满足此 expr 的 totalPositiveRows 的百分比; //如果总计 100 行中有 60 行得分>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;
    
  3. Set initialexpr=选择具有最大值 diffPercentage 的 expr set initalPositivePercentage=选择相应的positivePercentage值; set initalNegativePercentage=选择相应的negativePercentage值; 我的想法是,我现在需要继续扩展 initalexpr,直到 initalNegativePercentage 变为 0。

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

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

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

  2. 选择给出最大负减少的表达式,成为新的初始表达式。 Set initialexpr=选择具有最大值 negativeReduction 的 expr set initalPositivePercentage=选择相应的值; set initalNegativePercentage=选择相应的值;

  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,因为示例不好,并且条件 和 columnB >10 不正确。 Col04 仅用于演示,因为它是中性的。您需要在 java 或 sql 中将输出字符串连接在一起,但这应该不是问题。

我将基础 table 命名为 scores,然后创建了视图 positives,你可以代替视图将数据放在一些临时的 table 中,执​​行速度应该快得多.

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 版本 < 11,请将 listagg 替换为 wmsys.wm_concat。

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

基本思想是交叉连接每个表达式与列的每个不同值,然后交叉连接所有列。 table 将与每个表达式列表一起查询,并为正面和负面分数生成计数。如果表达式 return 是预期的正分数和 none 的负分数,则它是有效的。

这假设您只使用表达式 ><=。每个新列或表达式都会使这个问题呈指数级变慢。

测试架构

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%%)(.*)', 'where 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 次查询。结果可能包括重复项,这些重复项对于减少来说非常重要。或者,也许你会很幸运,解决方案太少了,你可以盯着它们看。

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

优化

没有return任何阳性结果的个别表达被排除在外。对于示例数据,这将查询执行次数从 6370 次减少到 441 次。

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

但即使是 100 倍的性能提升也可能无助于解决 NP 完全问题。您可能需要根据您的示例数据查找一些额外的 "short cuts"。

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

如果估计是十亿年,而你想不出任何让蛮力法更快工作的技巧,也许是时候在 https://cstheory.stackexchange.com/[=20= 上问这个问题了]

我会这样做:

  1. 检查每个 "input" 列的最小值和最大值
  2. 检查分数 > 0
  3. 的子集的每个 "input" 列的最小值和最大值

现在,对于每个 "input" 列:

  1. 如果点 1 和点 2 的最小值和最大值相同,则该列没有方位角
  2. 否则,如果点 2 的最小值和最大值相同,则该列为“=”
  3. 否则,如果最小值相同但最大值不同,则该列为“<”,以点 2 的最大值作为参考
  4. 否则,如果最大值相同但最小值不同,则该列为“>”,以点 2 的最小值作为参考
  5. 否则,该列是“< AND >”

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

SQL 为任意模式生成查询以输出上述条件应该相对容易构建。如果您需要帮助,请告诉我,我会调查一下。

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;

这将为您提供每列正分的范围,然后是这些列对所有分数的范围。如果这些范围不同,则存在相关性

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

准备: 创建所有可能的表达式列表,如 A=0、B>10、C<5(对于 40 列,我最终确定了总共约 150 个表达式)

我们称它为"expressions"变量。

第一个 运行 的算法:

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。

后续 运行s 的算法,直到 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.   

请评论。

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

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

  • 对于第一列,解决方案是:A=1 => false, A=0 => true.
  • 然后添加第二个维度 B。您有 5 个值,因此 B 列上的维度被分为 6 个区间。可以加入一些连续的间隔。例如 <10, 50> 和 <50,inf> 都表示 true。
  • 然后添加第 3 个维度。
  • ...

如果您想在 SQL 级别加入维度区间,您可以使用 LAG 函数。通过使用分区和窗口,您可以按一列对行进行排序。然后,您在熟列中计算一个值 true/false。在下一个熟列中,通过使用 LAG 函数,您可以检测 true/false 标志是否确实从上一行发生了变化。

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。因为这个与正分值和负分值都相关联。那么应该包含还是排除?

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

令 A 为所有产生正分的输入的集合,B 为所有未产生正分的输入的集合。

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

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