ใน PostgreSQL มีฟังก์ชั่นรวมที่ปลอดภัยชนิดแรก () หรือไม่?


21

คำถามแบบเต็มเขียนใหม่

ฉันกำลังมองหาฟังก์ชันการรวมครั้งแรก ()

ที่นี่ฉันพบบางสิ่งที่เกือบจะได้ผล:

CREATE OR REPLACE FUNCTION public.first_agg ( anyelement, anyelement )
RETURNS anyelement LANGUAGE sql IMMUTABLE STRICT AS $$
        SELECT $1;
$$;

-- And then wrap an aggregate around it
CREATE AGGREGATE public.first (
        sfunc    = public.first_agg,
        basetype = anyelement,
        stype    = anyelement
);

ปัญหาคือเมื่อคอลัมน์ varchar (n) ผ่านฟังก์ชั่นแรก () ก็จะถูกแปลงเป็น varchar ง่าย (ไม่มีขนาด) พยายามคืนแบบสอบถามในฟังก์ชั่นเป็น RETURNS SETOF anyelement ฉันได้รับข้อผิดพลาดดังต่อไปนี้

ข้อผิดพลาด: โครงสร้างของแบบสอบถามไม่ตรงกับประเภทผลลัพธ์ของฟังก์ชัน Estado de SQL: 42804 Detalhe: อักขระประเภทที่ส่งคืนไม่ตรงกับอักขระประเภทที่คาดว่าจะแปรเปลี่ยน (40) ในคอลัมน์ 2 บริบท: PL / pgSQL ฟังก์ชัน vsr_table_at_time (anyelement, timestamp ) บรรทัดที่ 31 ที่ RETURN QUERY

ในหน้า wiki เดียวกันมีลิงค์ไปยังC Version ของฟังก์ชันที่จะแทนที่ข้างต้น ฉันไม่รู้วิธีติดตั้ง แต่ฉันสงสัยว่าเวอร์ชันนี้สามารถแก้ปัญหาของฉันได้หรือไม่

ในขณะเดียวกันมีวิธีที่ฉันสามารถเปลี่ยนฟังก์ชั่นดังกล่าวข้างต้นเพื่อให้ผลตอบแทนที่แน่นอนประเภทเดียวกันของคอลัมน์อินพุต?

คำตอบ:


17

DISTINCT ON()

เช่นเดียวกับบันทึกย่อนี่คือสิ่งที่DISTINCT ON()ทำ (เพื่อไม่ให้สับสนDISTINCT)

SELECT DISTINCT ON ( expression [, ...] ) ช่วยให้เพียงแถวแรกของชุดของแต่ละแถวที่แสดงออกได้รับการประเมินให้เท่ากับ การDISTINCT ONแสดงออกที่ถูกตีความโดยใช้กฎเดียวกันกับORDER BY(ดูด้านบน) โปรดทราบว่า "แถวแรก" ของแต่ละชุดนั้นไม่สามารถคาดเดาได้เว้นแต่ORDER BYจะใช้เพื่อให้แน่ใจว่าแถวที่ต้องการจะปรากฏขึ้นก่อน ตัวอย่างเช่น

ดังนั้นถ้าคุณจะเขียน

SELECT myFirstAgg(z)
FROM foo
GROUP BY x,y;

มันมีประสิทธิภาพ

SELECT DISTINCT ON(x,y) z
FROM foo;
-- ORDER BY z;

zในการที่จะใช้เวลาแรก มีความแตกต่างที่สำคัญสองประการ

  1. นอกจากนี้คุณยังสามารถเลือกคอลัมน์อื่น ๆ ได้โดยไม่มีค่าใช้จ่ายในการรวมต่อไป ..

    SELECT DISTINCT ON(x,y) z, k, r, t, v
    FROM foo;
    -- ORDER BY z, k, r, t, v;
  2. เนื่องจากไม่มีGROUP BYคุณไม่สามารถใช้การรวม (จริง) กับมันได้

    CREATE TABLE foo AS
    SELECT * FROM ( VALUES
      (1,2,3),
      (1,2,4),
      (1,2,5)
    ) AS t(x,y,z);
    
    SELECT DISTINCT ON (x,y) z, sum(z)
    FROM foo;
    
    -- fails, as you should expect.
    SELECT DISTINCT ON (x,y) z, sum(z)
    FROM foo;
    
    -- would not otherwise fail.
    SELECT myFirstAgg(z), sum(z)
    FROM foo
    GROUP BY x,y;

อย่าลืม ORDER BY

นอกจากนี้ในขณะที่ฉันไม่กล้าทำตอนนี้ฉันก็จะเรียบร้อยแล้ว

โปรดทราบว่า "แถวแรก" ของแต่ละชุดนั้นไม่สามารถคาดเดาได้เว้นแต่ว่ามีการใช้ ORDER BY เพื่อให้แน่ใจว่าแถวที่ต้องการจะปรากฏขึ้นก่อน ตัวอย่างเช่น

ใช้เสมอORDER BYกับDISTINCT ON

ใช้ฟังก์ชั่นการรวมชุดสั่งซื้อ

ผมคิดว่าผู้คนจำนวนมากกำลังมองหาfirst_value, สั่งชุดรวมฟังก์ชั่น แค่อยากจะโยนมันออกไป มันจะมีลักษณะเช่นนี้หากมีฟังก์ชันอยู่:

SELECT a, b, first_value() WITHIN GROUP (ORDER BY z)    
FROM foo
GROUP BY a,b;

แต่อนิจจาคุณสามารถทำได้

SELECT a, b, percentile_disc(0) WITHIN GROUP (ORDER BY z)   
FROM foo
GROUP BY a,b;

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

6

ใช่ฉันพบวิธีที่ง่ายกับกรณีของคุณโดยใช้คุณสมบัติบางอย่างใน PostgreSQL 9.4+

ลองดูตัวอย่างนี้:

select  (array_agg(val ORDER BY i))[1] as first_value_orderby_i,
    (array_agg(val ORDER BY i DESC))[1] as last_value_orderby_i,
    (array_agg(val))[1] as last_value_all,
    (array_agg(val))[array_length(array_agg(val),1)] as last_value_all
   FROM (
        SELECT i, random() as val
        FROM generate_series(1,100) s(i)
        ORDER BY random()
    ) tmp_tbl

ฉันหวังว่ามันจะช่วยคุณในกรณีของคุณ


problm พร้อมโซลูชันนี้คือมันไม่ทำงานกับDOMAINชนิดข้อมูลหรือข้อยกเว้นขนาดเล็กอื่น ๆ นอกจากนี้ยังมีความซับซ้อนและเสียเวลามากขึ้นในการสร้างชุดข้อมูลทั้งหมด วิธีแก้ปัญหาง่ายๆคือการสร้างการรวมแบบกำหนดเอง แต่จนถึงตอนนี้ฉันยังไม่พบโซลูชันที่สมบูรณ์แบบแม้จะเป็นแบบนั้นก็ตาม ฟังก์ชั่นหน้าต่างก็ไม่ดีเช่นกันเนื่องจากมันไม่สามารถใช้แบบเดียวกับที่คุณใช้มวลรวม (ด้วยคำสั่ง FILTER หรือใน CROSS JOIN LATERAL)
AlexanderMP

5

ไม่ใช่คำตอบสำหรับคำถามของคุณโดยตรง แต่คุณควรลองใช้first_valueฟังก์ชั่นหน้าต่าง มันทำงานได้เช่นนี้:

CREATE TABLE test (
    id SERIAL NOT NULL PRIMARY KEY,
    cat TEXT,
    value VARCHAR(2)
    date TIMESTAMP WITH TIME ZONE

);

จากนั้นหากคุณต้องการรายการแรกในแต่ละcatหมวดหมู่คุณจะค้นหาดังนี้

SELECT
    cat,
    first_value(date) OVER (PARTITION BY cat ORDER BY date)
FROM
    test;

หรือ:

SELECT
    cat,
    first_value(date) OVER w
FROM
    test
WINDOW w AS (PARTITION BY cat ORDER BY date);

ขออภัยฉันไม่คิดว่ากรณีนี้จะใช้กับกรณีการใช้งานของฉัน First_value ไม่ใช่ฟังก์ชันการรวมแสดงระเบียนทั้งหมดของด้วยค่าทั่วไปบางอย่าง (ตัวอย่าง cat ของคุณ) ที่ประเมินว่าเป็นรายการแรกตามลำดับ (วันที่ตัวอย่างของคุณ) ความต้องการของฉันแตกต่าง ฉันต้องการในการเลือกเดียวกันรวมหลายคอลัมน์โดยเลือกค่าแรกไม่ใช่ค่า null นั่นคือควรส่งออกระเบียนเดียวสำหรับแต่ละชุดค่าใน GROUP BY
Alexandre Neto

2
select distinct x, first_value(y) over (partition by x), first_value(z) over (partition by x) from ...ดังกล่าวข้างต้นสามารถทำเพื่อการทำงานที่แตกต่างกันโดยการขว้างปาลงไปในส่วนผสม: อาจไม่มีประสิทธิภาพ แต่เพียงพอสำหรับฉันที่จะได้รับในการสร้างต้นแบบ มีบางอย่างที่ต้องกลับมาอีกแน่นอน!
Max Murphy
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.