ค้นหาแถวที่มีเลขจำนวนเต็มตามลำดับที่กำหนด


9

ปัญหา

หมายเหตุ: ผมหมายถึงลำดับคณิตศาสตร์ไม่ใช่กลไกลำดับของ PostgreSQL

ฉันมีตารางที่แสดงลำดับของจำนวนเต็ม ความหมายคือ:

CREATE TABLE sequences
(
  id serial NOT NULL,
  title character varying(255) NOT NULL,
  date date NOT NULL,
  sequence integer[] NOT NULL,
  CONSTRAINT "PRIM_KEY_SEQUENCES" PRIMARY KEY (id)
);

เป้าหมายของฉันคือการหาแถวโดยใช้ลำดับที่กำหนด กล่าวคือแถวที่sequenceเขตข้อมูลเป็นลำดับที่มีการเรียงลำดับที่กำหนด (ในกรณีของฉันลำดับจะเรียงตามลำดับ)

ตัวอย่าง

สมมติว่าตารางมีข้อมูลต่อไปนี้:

+----+-------+------------+-------------------------------+
| id | title |    date    |           sequence            |
+----+-------+------------+-------------------------------+
|  1 | BG703 | 2004-12-24 | {1,3,17,25,377,424,242,1234}  |
|  2 | BG256 | 2005-05-11 | {5,7,12,742,225,547,2142,223} |
|  3 | BD404 | 2004-10-13 | {3,4,12,5698,526}             |
|  4 | BK956 | 2004-08-17 | {12,4,3,17,25,377,456,25}     |
+----+-------+------------+-------------------------------+

ดังนั้นหากการเรียงลำดับที่กำหนดคือ{12, 742, 225, 547}ฉันต้องการหาแถวที่ 2

ในทำนองเดียวกันถ้าการเรียงลำดับที่กำหนดคือ{3, 17, 25, 377}ฉันต้องการหาแถว 1 และ 4 แถว

ในที่สุดหากการเรียงลำดับที่กำหนดคือ{12, 4, 3, 25, 377}จากนั้นจะไม่มีการส่งคืนแถว

สืบสวน

อันดับแรกฉันไม่แน่ใจว่าการเรียงลำดับด้วยอาร์เรย์ชนิดข้อมูลนั้นฉลาดหรือไม่ แม้ว่ามันจะดูเหมาะสมกับสถานการณ์ก็ตาม ฉันกลัวว่ามันจะทำให้การจัดการที่ซับซ้อนมากขึ้น บางทีอาจเป็นการดีกว่าที่จะแสดงลำดับต่างกันโดยใช้รูปแบบความสัมพันธ์กับตารางอื่น

ในทำนองเดียวกันฉันคิดว่าจะขยายลำดับโดยใช้unnestฟังก์ชันอาร์เรย์แล้วเพิ่มเกณฑ์การค้นหาของฉัน อย่างไรก็ตามจำนวนคำศัพท์ในลำดับนั้นเป็นตัวแปรฉันไม่เห็นวิธีการทำเช่นนั้น

ฉันรู้ว่ามันเป็นไปได้ที่จะตัดลำดับของฉันตามลำดับโดยใช้subarrayฟังก์ชั่นของโมดูลintarrayแต่ฉันไม่เห็นว่ามันมีประโยชน์กับฉันอย่างไรสำหรับการค้นหาของฉัน

ข้อ จำกัด

แม้ว่าในขณะนี้แบบจำลองของฉันยังคงได้รับการพัฒนาตารางก็มีจุดประสงค์ที่จะประกอบไปด้วยหลายลำดับระหว่าง 50,000 ถึง 300,000 แถว ดังนั้นฉันมีข้อ จำกัด ด้านประสิทธิภาพที่แข็งแกร่ง

ในตัวอย่างของฉันฉันใช้จำนวนเต็มค่อนข้างน้อย bigintในทางปฏิบัติมันเป็นไปได้ว่าจำนวนเต็มเหล่านี้กลายเป็นมีขนาดใหญ่มากขึ้นไปล้น ในสถานการณ์เช่นนี้ฉันคิดว่าวิธีที่ดีที่สุดคือการจัดเก็บตัวเลขเป็นสตริง (เนื่องจากไม่จำเป็นต้องดำเนินการตามลำดับของการดำเนินการทางคณิตศาสตร์เหล่านี้) อย่างไรก็ตามการเลือกใช้โซลูชันนี้ทำให้ไม่สามารถใช้โมดูลintarray ที่กล่าวถึงข้างต้น


หากพวกเขาสามารถล้นbigintคุณควรใช้numericเป็นประเภทในการจัดเก็บพวกเขา มันช้ากว่ามากและใช้พื้นที่มากขึ้น
Craig Ringer

@CraigRinger ทำไมต้องใช้numericและไม่ใช่สตริง ( textตัวอย่าง)? ฉันไม่จำเป็นต้องดำเนินการทางคณิตศาสตร์กับลำดับของฉัน
mlpo

2
เพราะมันมีขนาดกะทัดรัดและเร็วกว่าหลายวิธีtextและป้องกันไม่ให้คุณจัดเก็บข้อมูลที่ไม่ใช่ตัวเลขปลอม ขึ้นอยู่กับว่าหากคุณทำ I / O เพียงอย่างเดียวคุณอาจต้องการข้อความเพื่อลดการประมวลผล I / O
Craig Ringer

@ CraigRinger แน่นอนประเภทนี้สอดคล้องกันมากขึ้น เกี่ยวกับประสิทธิภาพฉันจะทดสอบเมื่อฉันพบวิธีทำการค้นหา
mlpo

2
@CraigRinger มันอาจทำงานได้ถ้าคำสั่งนั้นไม่สำคัญ แต่ที่นี่ลำดับมีการสั่งซื้อ ตัวอย่าง: SELECT ARRAY[12, 4, 3, 17, 25, 377, 456, 25] @> ARRAY[12, 4, 3, 25, 377];จะคืนค่าจริงเนื่องจากออเดอร์นี้ไม่ได้รับการพิจารณา
mlpo

คำตอบ:


3

หากคุณกำลังมองหาการปรับปรุงประสิทธิภาพที่สำคัญสำหรับคำตอบของ dnoeth ลองใช้ฟังก์ชั่น C ดั้งเดิมและสร้างตัวดำเนินการที่เหมาะสม

นี่คือตัวอย่างสำหรับ int4 arrays ( ตัวแปรอาร์เรย์ทั่วไปและสคริปต์ SQL ที่สอดคล้องกัน )

Datum
_int_sequence_contained(PG_FUNCTION_ARGS)
{
    return DirectFunctionCall2(_int_contains_sequence,
                               PG_GETARG_DATUM(1),
                               PG_GETARG_DATUM(0));
}

Datum
_int_contains_sequence(PG_FUNCTION_ARGS)
{
    ArrayType  *a = PG_GETARG_ARRAYTYPE_P(0);
    ArrayType  *b = PG_GETARG_ARRAYTYPE_P(1);
    int         na, nb;
    int32      *pa, *pb;
    int         i, j;

    na = ArrayGetNItems(ARR_NDIM(a), ARR_DIMS(a));
    nb = ArrayGetNItems(ARR_NDIM(b), ARR_DIMS(b));
    pa = (int32 *) ARR_DATA_PTR(a);
    pb = (int32 *) ARR_DATA_PTR(b);

    /* The naive searching algorithm. Replace it with a better one if your arrays are quite large. */
    for (i = 0; i <= na - nb; ++i)
    {
        for (j = 0; j < nb; ++j)
            if (pa[i + j] != pb[j])
                break;

        if (j == nb)
            PG_RETURN_BOOL(true);
    }

    PG_RETURN_BOOL(false);
}
CREATE FUNCTION _int_contains_sequence(_int4, _int4)
RETURNS bool
AS 'MODULE_PATHNAME'
LANGUAGE C STRICT IMMUTABLE;

CREATE FUNCTION _int_sequence_contained(_int4, _int4)
RETURNS bool
AS 'MODULE_PATHNAME'
LANGUAGE C STRICT IMMUTABLE;

CREATE OPERATOR @@> (
  LEFTARG = _int4,
  RIGHTARG = _int4,
  PROCEDURE = _int_contains_sequence,
  COMMUTATOR = '<@@',
  RESTRICT = contsel,
  JOIN = contjoinsel
);

CREATE OPERATOR <@@ (
  LEFTARG = _int4,
  RIGHTARG = _int4,
  PROCEDURE = _int_sequence_contained,
  COMMUTATOR = '@@>',
  RESTRICT = contsel,
  JOIN = contjoinsel
);

ตอนนี้คุณสามารถกรองแถวเช่นนี้

SELECT * FROM sequences WHERE sequence @@> '{12, 742, 225, 547}'

ฉันได้ทำการทดลองเล็กน้อยเพื่อค้นหาว่าโซลูชันนี้เร็วขึ้นเท่าใด

CREATE TEMPORARY TABLE sequences AS
SELECT array_agg((random() * 10)::int4) AS sequence, g1 AS id
FROM generate_series(1, 100000) g1
  CROSS JOIN generate_series(1, 30) g2
GROUP BY g1;
EXPLAIN ANALYZE SELECT * FROM sequences
WHERE        translate(cast(sequence as text), '{}',',,')
 LIKE '%' || translate(cast('{1,2,3,4}'as text), '{}',',,') || '%'

"Seq Scan on sequences  (cost=0.00..7869.42 rows=28 width=36) (actual time=2.487..334.318 rows=251 loops=1)"
"  Filter: (translate((sequence)::text, '{}'::text, ',,'::text) ~~ '%,1,2,3,4,%'::text)"
"  Rows Removed by Filter: 99749"
"Planning time: 0.104 ms"
"Execution time: 334.365 ms"
EXPLAIN ANALYZE SELECT * FROM sequences WHERE sequence @@> '{1,2,3,4}'

"Seq Scan on sequences  (cost=0.00..5752.01 rows=282 width=36) (actual time=0.178..20.792 rows=251 loops=1)"
"  Filter: (sequence @@> '{1,2,3,4}'::integer[])"
"  Rows Removed by Filter: 99749"
"Planning time: 0.091 ms"
"Execution time: 20.859 ms"

ดังนั้นจึงเร็วกว่าประมาณ 16 เท่า หากยังไม่เพียงพอคุณสามารถเพิ่มการสนับสนุนสำหรับดัชนี GIN หรือ GiST แต่นี่จะเป็นงานที่ยากมาก


เสียงที่น่าสนใจ แต่ฉันใช้สตริงหรือประเภทเพื่อแสดงข้อมูลของฉันเพราะพวกเขาอาจล้นnumeric bigintมันอาจเป็นการดีที่จะแก้ไขคำตอบของคุณเพื่อให้ตรงกับข้อ จำกัด ของคำถาม อย่างไรก็ตามฉันจะทำการเปรียบเทียบที่ฉันจะโพสต์ที่นี่
mlpo

ฉันไม่แน่ใจว่าเป็นการดีหรือไม่ที่จะวางโค้ดขนาดใหญ่ลงในคำตอบเนื่องจากพวกเขาควรจะมีขนาดเล็กที่สุด รุ่นอาเรย์ทั่วไปของฟังก์ชั่นนี้ยาวกว่าสี่เท่าและค่อนข้างยุ่งยาก ฉันได้ทำการทดสอบด้วยnumericและtextยังมีการปรับปรุงตั้งแต่ 20 ถึง 50 เท่าขึ้นอยู่กับความยาวของอาร์เรย์
Slonopotamus

ใช่ แต่มันเป็นสิ่งจำเป็นที่คำตอบตอบคำถาม :-) ที่นี่ฉันคิดว่าคำตอบที่ปฏิบัติตามข้อ จำกัด นั้นน่าสนใจ (เพราะแง่มุมนี้เป็นส่วนหนึ่งของคำถาม) อย่างไรก็ตามอาจไม่จำเป็นต้องเสนอรุ่นทั่วไป numericเพียงแค่รุ่นที่มีสายหรือ
mlpo

อย่างไรก็ตามฉันได้เพิ่มเวอร์ชันสำหรับอาร์เรย์ทั่วไปเนื่องจากมันเกือบจะเหมือนกันสำหรับชนิดข้อมูลที่มีความยาวผันแปรใด ๆ bigintแต่ถ้าคุณมีความกังวลมากเกี่ยวกับประสิทธิภาพการทำงานคุณควรติดกับชนิดข้อมูลขนาดคงที่เช่น
Slonopotamus

ฉันชอบที่จะทำเช่นนั้น ปัญหาคือว่าบางส่วนของลำดับของฉันล้นเกินกว่าbigintดังนั้นดูเหมือนว่าฉันไม่มีทางเลือก แต่ถ้าคุณมีความคิดฉันสนใจ :)
mlpo

1

คุณสามารถหาส่วนประกอบได้อย่างง่ายดายเมื่อคุณใช้อาร์เรย์เป็นสตริงและแทนที่วงเล็บปีกกาด้วยเครื่องหมายจุลภาค:

translate(cast(sequence as varchar(10000)), '{}',',,')

{1,3,17,25,377,424,242,1234} -> ',1,3,17,25,377,424,242,1234,'

ทำเช่นเดียวกันกับอาร์เรย์ที่คุณค้นหาและเพิ่มการนำหน้าและส่วนท้าย%:

'%' || translate(cast(searchedarray as varchar(10000)), '{}',',,') || '%'

{3, 17, 25, 377} -> '%,3,17,25,377,%'

ตอนนี้คุณเปรียบเทียบโดยใช้LIKE:

WHERE        translate(cast(sequence      as varchar(10000)), '{}',',,')
 LIKE '%' || translate(cast(searchedarray as varchar(10000)), '{}',',,') || '%'

แก้ไข:

ซอทำงานอีกครั้ง

หากอาร์เรย์มีการทำให้เป็นมาตรฐานในหนึ่งแถวต่อค่าคุณสามารถใช้ตรรกะตามชุด:

CREATE TABLE sequences
( id int NOT NULL,
  n int not null,
  val numeric not null
);

insert into sequences values(  1, 1,1     );
insert into sequences values(  1, 2,3     );
insert into sequences values(  1, 3,17    );
insert into sequences values(  1, 4,25    );
insert into sequences values(  1, 5,377   );
insert into sequences values(  1, 6,424   );
insert into sequences values(  1, 7,242   );
insert into sequences values(  1, 8,1234  );
insert into sequences values(  2, 1,5     );
insert into sequences values(  2, 2,7     );
insert into sequences values(  2, 3,12    );
insert into sequences values(  2, 4,742   );
insert into sequences values(  2, 5,225   );
insert into sequences values(  2, 6,547   );
insert into sequences values(  2, 7,2142  );
insert into sequences values(  2, 8,223   );
insert into sequences values(  3, 1,3     );
insert into sequences values(  3, 2,4     );
insert into sequences values(  3, 3,12    );
insert into sequences values(  3, 4,5698  );
insert into sequences values(  3, 5,526   );          
insert into sequences values(  4, 1,12    );
insert into sequences values(  4, 2,4     );
insert into sequences values(  4, 3,3     );
insert into sequences values(  4, 4,17    );
insert into sequences values(  4, 5,25    );
insert into sequences values(  4, 6,377   );
insert into sequences values(  4, 7,456   );
insert into sequences values(  4, 8,25    );
insert into sequences values(  5, 1,12    );
insert into sequences values(  5, 2,4     );
insert into sequences values(  5, 3,3     );
insert into sequences values(  5, 4,17    );
insert into sequences values(  5, 5,17    );
insert into sequences values(  5, 6,25    );
insert into sequences values(  5, 7,377   );
insert into sequences values(  5, 8,456   );
insert into sequences values(  5, 9,25    );

nต้องต่อเนื่องไม่ซ้ำซ้อนไม่มีช่องว่าง ตอนนี้เข้าร่วมค่าทั่วไปและใช้ประโยชน์จากความจริงที่ว่าลำดับเป็นลำดับ :-)

with searched (n,val) as (
  VALUES
   ( 1,3  ),
   ( 2,17 ),
   ( 3,25 ),
   ( 4,377)
)
select seq.id, 
   -- this will return the same result if the values from both tables are in the same order
   -- it's a meaningless dummy, but the same meaningless value for sequential rows 
   seq.n - s.n as dummy,
   seq.val,
   seq.n,
   s.n 
from sequences as seq join searched as s
on seq.val = s.val
order by seq.id, dummy, seq.n;

ในที่สุดก็นับจำนวนแถวด้วยดัมมี่เดียวกันและตรวจสอบว่าเป็นจำนวนที่ถูกต้องหรือไม่:

with searched (n,val) as (
  VALUES
   ( 1,3  ),
   ( 2,17 ),
   ( 3,25 ),
   ( 4,377)
)
select distinct seq.id
from sequences as seq join searched as s
on seq.val = s.val
group by 
   seq.id,
   seq.n - s.n
having count(*) = (select count(*) from searched)
;

ลองทำดัชนีตามลำดับ (val, id, n)


ฉันยังพิจารณาโซลูชันนี้ในภายหลัง แต่ฉันเห็นปัญหาหลายอย่างที่ดูค่อนข้างน่ารำคาญ: อย่างแรกเลยฉันกลัวว่าวิธีนี้ไม่มีประสิทธิภาพมากเราต้องเลือกแต่ละแถวของแต่ละแถวก่อนทำการค้นหา เป็นไปได้ที่จะพิจารณาจัดเก็บลำดับในTEXTฟิลด์ ( varcharเป็นความคิดที่ไม่ดีในความคิดของฉันลำดับอาจยาวตามจำนวนดังนั้นขนาดค่อนข้างคาดเดาไม่ได้) เพื่อหลีกเลี่ยงการร่าย; แต่ก็ยังเป็นไปไม่ได้ที่จะใช้ดัชนีเพื่อปรับปรุงการแสดง (นอกจากนี้การใช้ฟิลด์สตริงไม่จำเป็นต้องรอบคอบดูความคิดเห็นของ @CraigRinger ด้านบน)
mlpo

@mlpo: ความคาดหวังในการทำงานของคุณคืออะไร? เพื่อให้สามารถใช้ดัชนีได้คุณจะต้องทำให้มาตรฐานเป็นหนึ่งแถวต่อค่าหนึ่งให้ใช้แผนกสัมพันธ์และสุดท้ายตรวจสอบว่าคำสั่งนั้นถูกต้องหรือไม่ ในตัวอย่างของคุณ25มีอยู่สองครั้งid=4มันเป็นไปได้จริงหรือ มีการจับคู่โดยเฉลี่ย / มากที่สุดสำหรับลำดับการค้นหาหรือไม่
dnoeth

ลำดับอาจมีจำนวนเท่ากัน ตัวอย่างเช่น{1, 1, 1, 1, 12, 2, 2, 12, 12, 1, 1, 5, 4}เป็นไปได้ค่อนข้าง เกี่ยวกับจำนวนการแข่งขันลำดับที่ใช้มักจะคิดว่าจะ จำกัด จำนวนผลลัพธ์ อย่างไรก็ตามบางลำดับอาจคล้ายกันมากและบางครั้งมันก็น่าสนใจที่จะใช้ลำดับที่สั้นลงเพื่อให้ได้ผลลัพธ์มากขึ้น ฉันประเมินว่าจำนวนการจับคู่สำหรับกรณีส่วนใหญ่อยู่ระหว่าง 0 ถึง 100 ด้วยความเป็นไปได้ที่บางครั้งการเรียงลำดับการจับคู่กับลำดับจำนวนมากมักจะสั้นหรือสั้นมาก
mlpo

@mlpo: ผมเพิ่มวิธีการแก้ปัญหาการติดตั้งและฉันจะสนใจมากในการเปรียบเทียบผลการดำเนินงานบาง :-)
dnoeth

@ypercube: นี่เป็นเพียงการเพิ่มที่รวดเร็วเพื่อให้ได้ผลลัพธ์ที่มีความหมายมากขึ้น :-) โอเคมันน่ากลัวฉันจะเปลี่ยนมัน l
dnoeth
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.