【问题标题】:Get all possible combinations from array elements with efficient way以有效的方式从数组元素中获取所有可能的组合
【发布时间】:2018-07-24 09:30:42
【问题描述】:

其中有最多 5 个元素的数组,例如给定:{1,2,3,4,5},我需要从该数组中获取所有可能的唯一组合,预期结果是:

 {1}
 {1,2}
 {1,2,3}
 {1,2,3,4}
 {1,2,3,4,5}
 {1,2,3,5}
 {1,2,4}
 {1,2,4,5}
 {1,2,5}
 {1,3}
 {1,3,4}
 {1,3,4,5}
 {1,3,5}
 {1,4}
 {1,4,5}
 {1,5}
 {2}
 {2,3}
 {2,3,4}
 {2,3,4,5}
 {2,3,5}
 {2,4}
 {2,4,5}
 {2,5}
 {3}
 {3,4}
 {3,4,5}
 {3,5}
 {4}
 {4,5}
 {5}

我有这个解决方案:

create table temp_all_possible_cards (
    card_ids int[]
);

create or replace function test(cards_in_hands INT[] )
returns void
as $$
begin 
    with all_possible_cards(ids) as(
        select ARRAY_APPEND('{}'::int[], t1.card_ids)||ARRAY_APPEND('{}'::int[], t2.card_ids)||ARRAY_APPEND('{}'::int[], t3.card_ids)||ARRAY_APPEND('{}'::int[], t4.card_ids)||ARRAY_APPEND('{}'::int[], t5.card_ids)
        from (
            select unnest(cards_in_hands) as card_ids
        ) t1
        cross join (
            select unnest(cards_in_hands) as card_ids
        ) t2
        cross join (
            select unnest(cards_in_hands) as card_ids
        ) t3
        cross join (
            select unnest(cards_in_hands) as card_ids
        ) t4
        cross join (
            select unnest(cards_in_hands) as card_ids
        ) t5
    )
    INSERT INTO temp_all_possible_cards
    SELECT DISTINCT uniq( sort(ids) ) from all_possible_cards;  
end;
$$ language plpgsql

这可行,但有一个大问题,有时我需要运行此功能 5000 次

do $$
begin
    for i in 1..5000 loop
        perform test('{1,2,3,4,5}');
    end loop;
end;
$$ language plpgsql

循环的执行时间为 55-60 秒。

问题:如何以有效的方式从数组中获取所有可能的唯一组合?如何优化这个方案,让 5000 次调用也明显快于 60 秒?

【问题讨论】:

  • 嗯,我想你的交叉连接方法已经是解决这个问题的好方法了,尽管我不确定我在临时表中的插入是必要的,并且可以作为 SQL 函数而不是一个plPgsql。但如果可能输入的数量是有限的,您可以预先计算并将结果保存在表格中。因此,稍后您只需选择与给定输入匹配的记录。

标签: sql postgresql combinations postgresql-9.6


【解决方案1】:
;WITH NOS AS (SELECT 1 aval 
             UNION ALL
             SELECT aval + 1 FROM NOS WHERE aval < 2 * 2 * 2 * 2 * 2 - 1
             )
    SELECT LEFT(IQ.x, LEN(IQ.x) - 1) + '}' FROM (
            SELECT RTRIM('{' 
                                + CASE WHEN aval & 1 != 0 THEN '1, ' ELSE '' END        
                                + CASE WHEN aval & 2 != 0 THEN '2, ' ELSE '' END        
                                + CASE WHEN aval & 4 != 0 THEN '3, ' ELSE '' END        
                                + CASE WHEN aval & 8 != 0 THEN '4, ' ELSE '' END        
                                + CASE WHEN aval & 16 != 0 THEN '5, ' ELSE '' END) AS X     
                        FROM NOS) IQ

显示基准

create table #test (x nvarchar(50))

declare @i int = 0; 

    declare @s datetime2 = sysutcdatetime();

    while @i < 5000
    begin

    ;WITH NOS AS (SELECT 1 aval 
                 UNION ALL
                 SELECT aval + 1 FROM NOS WHERE aval < 2 * 2 * 2 * 2 * 2 - 1
                 )
        insert #test SELECT LEFT(IQ.x, LEN(IQ.x) - 1) + '}' FROM (
                SELECT RTRIM('{' 
                                    + CASE WHEN aval & 1 != 0 THEN '1, ' ELSE '' END        
                                    + CASE WHEN aval & 2 != 0 THEN '2, ' ELSE '' END        
                                    + CASE WHEN aval & 4 != 0 THEN '3, ' ELSE '' END        
                                    + CASE WHEN aval & 8 != 0 THEN '4, ' ELSE '' END        
                                    + CASE WHEN aval & 16 != 0 THEN '5, ' ELSE '' END) AS X     
                            FROM NOS) IQ


        set @i = @i + 1;

        end 

        DECLARe @usTiming BIGINT = datediff(MICROSECOND, @s ,sysutcdatetime())

        select CAST(@usTiming as nvarchar(19)) + 'us = ' + CAST(CAST(@usTiming/1000000.000000000000 as dec(10,3)) as nvarchar(20)) + ' seconds';

        drop table #test

我有 2.5 秒

【讨论】:

  • 对不起,我刚刚注意到您正在使用 postgresql - 但按位运算符的想法可能仍然适合您
  • NOS部分可以通过填充表格来实现,如果你没有WITH
  • 很好,我会为 postgres 修改这个,只是 2 * 2 * 2 * 2 * 2 - 1 这个“公式”总是有效吗?对于 X 元素计数,2 * X times - 1 总是给出唯一组合计数?
  • 它通过二进制工作,因此对于 5 个项目,没有选择是 00000,所有选择都是 11111。您实际上从 00001 开始。从 1 到 31 的“二进制”计数给出了所有开/关组合。如果您有 10 个项目,那么您需要使用 10 个二进制数字(最大值 1023)。 & 运算符只是找出是否存在二进制数字
  • 并在 PostgreSQL 中生成一系列 1 - 32 - generate_series 有什么问题 - 比递归 SQL 好得多!
【解决方案2】:

plpython 函数中使用itertools

create or replace function generate_combinations(cards_in_hand int[])
returns void language plpython3u as $$

import itertools

plan = plpy.prepare("insert into temp_all_possible_cards values ($1)", ["int[]"])

for r in range(1, len(cards_in_hand) + 1):
    for i in itertools.combinations(cards_in_hand, r):
        plpy.execute(plan, [i])
$$;

这应该比 plpgsql 函数快几倍。

【讨论】:

    【解决方案3】:

    对于 5000 个整数数组,此解决方案大约需要 1.5 秒(我的测试数据表包含 5000 个长度在 1 到 5 之间的不同数组,每个元素值在 1 到 150 之间)

    这个想法是生成从 1 到长度(数组)的所有可能的二进制值。然后我只需要从对应的二进制字符串中存在 1 的数组中取出元素。

    A 获取测试数据

    B 生成二进制值。因为bin(5) 演员表不采用可变长度,所以我需要子字符串函数将位值长度标准化为数组长度。

    C 取消嵌套对应数组值旁边的二进制值。现在我可以过滤掉不应该出现在组合结果中的数组元素。

    D 重新聚合过滤后的元素。

    SELECT 
        orig, 
        array_agg(single_char) -- D
    FROM ( 
        SELECT *
        FROM (
            SELECT
                no,
                orig,
                unnest(regexp_split_to_array(no, '')) ::BOOLEAN AS to_set, -- C
                unnest(orig)         AS single_char
            FROM (
                SELECT
                    substring(generate_series(1,(2^len_a - 1) ::INTEGER)::bit(5)::text, 5 - len_a + 1, 5) AS no, --B
                    s.a AS orig
                FROM (                   
                    SELECT 
                        s.a, 
                        array_length(s.a, 1) as len_a 
                    FROM (
                        SELECT arrays::int[] a FROM testdata.arrays ORDER BY a -- A
                    ) s               
                ) s
            ) s
        ) s
        WHERE
            to_set
        AND single_char IS NOT NULL
    ) s
    GROUP BY orig, no
    

    例子:

    表testdata包含

    {1}
    {1,1,118}
    {1,2,70,142,11}
    

    查询结果是

    orig                  array_agg             
    --------------------  --------------------  
    {1}                   {1}                   
    {1,1,118}             {118}                 
    {1,1,118}             {1}                   
    {1,1,118}             {1,118}               
    {1,1,118}             {1}                   
    {1,1,118}             {118,1}               
    {1,1,118}             {1,1}                 
    {1,1,118}             {1,1,118}             
    {1,2,70,142,11}       {11}                  
    {1,2,70,142,11}       {142}                 
    {1,2,70,142,11}       {142,11}              
    {1,2,70,142,11}       {70}                  
    {1,2,70,142,11}       {70,11}               
    {1,2,70,142,11}       {70,142}              
    {1,2,70,142,11}       {70,142,11}           
    {1,2,70,142,11}       {2}                   
    {1,2,70,142,11}       {2,11}                
    {1,2,70,142,11}       {2,142}               
    {1,2,70,142,11}       {142,11,2}            
    {1,2,70,142,11}       {2,70}                
    {1,2,70,142,11}       {2,70,11}             
    {1,2,70,142,11}       {2,70,142}            
    {1,2,70,142,11}       {2,70,142,11}         
    {1,2,70,142,11}       {1}                   
    {1,2,70,142,11}       {1,11}                
    {1,2,70,142,11}       {1,142}               
    {1,2,70,142,11}       {1,142,11}            
    {1,2,70,142,11}       {1,70}                
    {1,2,70,142,11}       {1,70,11}             
    {1,2,70,142,11}       {1,70,142}            
    {1,2,70,142,11}       {142,11,1,70}         
    {1,2,70,142,11}       {1,2}                 
    {1,2,70,142,11}       {1,2,11}              
    {1,2,70,142,11}       {1,2,142}             
    {1,2,70,142,11}       {1,2,142,11}          
    {1,2,70,142,11}       {1,2,70}              
    {1,2,70,142,11}       {1,2,70,11}           
    {1,2,70,142,11}       {1,2,70,142}          
    {1,2,70,142,11}       {1,2,70,142,11}
    

    编辑:因为array_agg 默认情况下不保证任何顺序,所以结果数组包含正确的元素,但不一定按照原始顺序。

    也许这也可能受到挑战。所以我在 C 和 D 之间添加了一个row_number() 窗口函数作为位置指示器:

    SELECT *, row_number() OVER (partition by orig, no) as position
    

    现在我可以通过array_agg(single_char ORDER BY position) 来订购阵列。但是订购非常昂贵,而且需要一秒钟的时间。

    【讨论】:

      猜你喜欢
      • 2019-05-20
      • 2023-04-10
      • 1970-01-01
      • 2016-07-08
      • 1970-01-01
      • 1970-01-01
      • 2023-03-02
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多