รีเซ็ตผลรวมการรันตามคอลัมน์อื่น


10

กำลังพยายามคำนวณผลรวมสะสม แต่ควรรีเซ็ตเมื่อยอดรวมสะสมมากกว่าค่าคอลัมน์อื่น

create table #reset_runn_total
(
id int identity(1,1),
val int, 
reset_val int,
grp int
)

insert into #reset_runn_total
values 
(1,10,1),
(8,12,1),(6,14,1),(5,10,1),(6,13,1),(3,11,1),(9,8,1),(10,12,1)


SELECT Row_number()OVER(partition BY grp ORDER BY id)AS rn,*
INTO   #test
FROM   #reset_runn_total

รายละเอียดดัชนี:

CREATE UNIQUE CLUSTERED INDEX ix_load_reset_runn_total
  ON #test(rn, grp) 

ข้อมูลตัวอย่าง

+----+-----+-----------+-----+
| id | val | reset_val | Grp |
+----+-----+-----------+-----+
|  1 |   1 |        10 | 1   |
|  2 |   8 |        12 | 1   |
|  3 |   6 |        14 | 1   |
|  4 |   5 |        10 | 1   |
|  5 |   6 |        13 | 1   |
|  6 |   3 |        11 | 1   |
|  7 |   9 |         8 | 1   |
|  8 |  10 |        12 | 1   |
+----+-----+-----------+-----+ 

ผลลัพธ์ที่คาดหวัง

+----+-----+-----------------+-------------+
| id | val |    reset_val    | Running_tot |
+----+-----+-----------------+-------------+
|  1 |   1 | 10              |       1     |  
|  2 |   8 | 12              |       9     |  --1+8
|  3 |   6 | 14              |       15    |  --1+8+6 -- greater than reset val
|  4 |   5 | 10              |       5     |  --reset 
|  5 |   6 | 13              |       11    |  --5+6
|  6 |   3 | 11              |       14    |  --5+6+3 -- greater than reset val
|  7 |   9 | 8               |       9     |  --reset -- greater than reset val 
|  8 |  10 | 12              |      10     |  --reset
+----+-----+-----------------+-------------+

ค้นหา:

Recursive CTEผมได้ผลโดยใช้ คำถามเดิมอยู่ที่นี่/programming/42085404/reset-running-total-based-on-another-column

;WITH cte
     AS (SELECT rn,id,
                val,
                reset_val,
                grp,
                val                   AS running_total,
                Iif (val > reset_val, 1, 0) AS flag
         FROM   #test
         WHERE  rn = 1
         UNION ALL
         SELECT r.*,
                Iif(c.flag = 1, r.val, c.running_total + r.val),
                Iif(Iif(c.flag = 1, r.val, c.running_total + r.val) > r.reset_val, 1, 0)
         FROM   cte c
                JOIN #test r
                  ON r.grp = c.grp
                     AND r.rn = c.rn + 1)
SELECT *
FROM   cte 

มีทางเลือกที่ดีกว่าในการT-SQLไม่ใช้CLRหรือไม่?


ดีกว่าได้อย่างไร แบบสอบถามนี้มีประสิทธิภาพต่ำหรือไม่ ใช้เมตริกอะไร
Aaron Bertrand

@AaronBertrand - เพื่อความเข้าใจที่ดีขึ้นฉันได้โพสต์ข้อมูลตัวอย่างสำหรับกลุ่มเดียว ฉันต้องทำเช่นเดียวกันสำหรับรอบ50000กลุ่มที่มีรหัสของ60 3000000ดังนั้นจำนวนรวมของระเบียนจะอยู่ที่ประมาณ มั่นใจว่าจะได้ระดับดีในRecursive CTE 3000000จะอัปเดตตัวชี้วัดเมื่อฉันกลับไปที่สำนักงาน เราสามารถทำได้โดยใช้sum()Over(Order by)เหมือนที่คุณใช้ในบทความนี้sqlperformance.com/2012/07/t-sql-queries/running-totals
P ரதீப்

เคอร์เซอร์อาจทำได้ดีกว่า CTE แบบเรียกซ้ำ
paparazzo

คำตอบ:


6

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

วิธีหนึ่งที่จะคิดเกี่ยวกับปัญหาคือคุณสามารถรับผลลัพธ์สุดท้ายที่คุณต้องการหากคุณคำนวณผลรวมการวิ่งพื้นฐานตราบเท่าที่คุณสามารถลบผลรวมการวิ่งออกจากแถวก่อนหน้าที่ถูกต้อง ยกตัวอย่างเช่นในข้อมูลตัวอย่างของค่าสำหรับid4 running total of row 4 - the running total of row 3เป็น ค่าสำหรับid6 คือrunning total of row 6 - the running total of row 3เนื่องจากการรีเซ็ตยังไม่เกิดขึ้น ค่าสำหรับid7 คือrunning total of row 7 - the running total of row 6เป็นต้น

ฉันจะเข้าใกล้สิ่งนี้กับ T-SQL ในวง ฉันถูกพาตัวไปเล็กน้อยและคิดว่าฉันมีทางออกเต็มรูปแบบ สำหรับ 3 ล้านแถวและ 500 กลุ่มรหัสเสร็จใน 24 วินาทีบนเดสก์ท็อปของฉัน ฉันกำลังทดสอบกับ SQL Server 2016 Developer Edition ที่มี 6 vCPU ฉันใช้ประโยชน์จากการแทรกแบบขนานและการประมวลผลแบบขนานโดยทั่วไปดังนั้นคุณอาจจำเป็นต้องเปลี่ยนรหัสหากคุณใช้รุ่นที่เก่ากว่าหรือมีข้อ จำกัด DOP

ด้านล่างรหัสที่ฉันใช้ในการสร้างข้อมูล ช่วงบนVALและRESET_VALควรใกล้เคียงกับข้อมูลตัวอย่างของคุณ

drop table if exists reset_runn_total;

create table reset_runn_total
(
id int identity(1,1),
val int, 
reset_val int,
grp int
);

DECLARE 
@group_num INT,
@row_num INT;
BEGIN
    SET NOCOUNT ON;
    BEGIN TRANSACTION;

    SET @group_num = 1;
    WHILE @group_num <= 50000 
    BEGIN
        SET @row_num = 1;
        WHILE @row_num <= 60
        BEGIN
            INSERT INTO reset_runn_total WITH (TABLOCK)
            SELECT 1 + ABS(CHECKSUM(NewId())) % 10, 8 + ABS(CHECKSUM(NewId())) % 8, @group_num;

            SET @row_num = @row_num + 1;
        END;
        SET @group_num = @group_num + 1;
    END;
    COMMIT TRANSACTION;
END;

อัลกอริทึมมีดังนี้:

1) เริ่มต้นด้วยการแทรกแถวทั้งหมดด้วยผลรวมการทำงานมาตรฐานลงในตารางอุณหภูมิ

2) ในวง:

2a) สำหรับแต่ละกลุ่มให้คำนวณแถวแรกด้วยผลรวมสะสมที่อยู่เหนือค่า reset_value ที่เหลืออยู่ในตารางและเก็บรหัสผลรวมสะสมที่มีขนาดใหญ่เกินไปและผลรวมการทำงานก่อนหน้าที่มีขนาดใหญ่เกินไปในตารางอุณหภูมิ

2b) ลบแถวออกจากตาราง temp แรกลงในผลลัพธ์ temp table ที่IDน้อยกว่าหรือเท่ากับIDในตาราง temp ที่สอง ใช้คอลัมน์อื่น ๆ เพื่อปรับผลรวมสะสมที่ต้องการ

3) หลังจากการลบไม่มีการประมวลผลแถวอีกต่อไปให้เรียกใช้เพิ่มเติมDELETE OUTPUTในตารางผลลัพธ์ นี่สำหรับแถวท้ายกลุ่มที่ไม่เกินค่ารีเซ็ต

ฉันจะดำเนินการตามขั้นตอนวิธีการหนึ่งใน T-SQL ทีละขั้นตอน

เริ่มต้นด้วยการสร้างตารางชั่วคราวสักสองสามตาราง #initial_resultsเก็บข้อมูลดั้งเดิมพร้อมผลรวมการทำงานมาตรฐาน#group_bookkeepingได้รับการอัปเดตแต่ละวงเพื่อหาว่าแถวไหนที่สามารถเคลื่อนย้ายได้และ#final_resultsมีผลลัพธ์พร้อมการปรับยอดรวมการทำงานสำหรับการรีเซ็ต

CREATE TABLE #initial_results (
id int,
val int, 
reset_val int,
grp int,
initial_running_total int
);

CREATE TABLE #group_bookkeeping (
grp int,
max_id_to_move int,
running_total_to_subtract_this_loop int,
running_total_to_subtract_next_loop int,
grp_done bit, 
PRIMARY KEY (grp)
);

CREATE TABLE #final_results (
id int,
val int, 
reset_val int,
grp int,
running_total int
);

INSERT INTO #initial_results WITH (TABLOCK)
SELECT ID, VAL, RESET_VAL, GRP, SUM(VAL) OVER (PARTITION BY GRP ORDER BY ID) RUNNING_TOTAL
FROM reset_runn_total;

CREATE CLUSTERED INDEX i1 ON #initial_results (grp, id);

INSERT INTO #group_bookkeeping WITH (TABLOCK)
SELECT DISTINCT GRP, 0, 0, 0, 0
FROM reset_runn_total;

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

รหัสด้านล่างทำงานในลูปและอัปเดตตารางการทำบัญชี สำหรับแต่ละกลุ่มเราจำเป็นต้องได้รับการหาค่าสูงสุดIDที่ควรย้ายไปไว้ในตารางผลลัพธ์ เราต้องการผลรวมสะสมจากแถวนั้นเพื่อให้เราสามารถลบออกจากผลรวมการเริ่มต้น grp_doneคอลัมน์ถูกกำหนดเป็น 1 เมื่อไม่มีการทำงานใด ๆ grpที่ต้องทำสำหรับ

WITH UPD_CTE AS (
        SELECT 
        #grp_bookkeeping.GRP
        , MIN(CASE WHEN initial_running_total - #group_bookkeeping.running_total_to_subtract_next_loop > RESET_VAL THEN ID ELSE NULL END) max_id_to_update
        , MIN(#group_bookkeeping.running_total_to_subtract_next_loop) running_total_to_subtract_this_loop
        , MIN(CASE WHEN initial_running_total - #group_bookkeeping.running_total_to_subtract_next_loop > RESET_VAL THEN initial_running_total ELSE NULL END) additional_value_next_loop
        , CASE WHEN MIN(CASE WHEN initial_running_total - #group_bookkeeping.running_total_to_subtract_next_loop > RESET_VAL THEN ID ELSE NULL END) IS NULL THEN 1 ELSE 0 END grp_done
        FROM #group_bookkeeping 
        INNER JOIN #initial_results IR ON #group_bookkeeping.grp = ir.grp
        WHERE #group_bookkeeping.grp_done = 0
        GROUP BY #group_bookkeeping.GRP
    )
    UPDATE #group_bookkeeping
    SET #group_bookkeeping.max_id_to_move = uv.max_id_to_update
    , #group_bookkeeping.running_total_to_subtract_this_loop = uv.running_total_to_subtract_this_loop
    , #group_bookkeeping.running_total_to_subtract_next_loop = uv.additional_value_next_loop
    , #group_bookkeeping.grp_done = uv.grp_done
    FROM UPD_CTE uv
    WHERE uv.GRP = #group_bookkeeping.grp
OPTION (LOOP JOIN);

จริงๆไม่ใช่แฟนของLOOP JOINคำใบ้โดยทั่วไป แต่นี่เป็นคำถามง่าย ๆ และเป็นวิธีที่เร็วที่สุดในการได้รับสิ่งที่ฉันต้องการ ในการปรับให้เหมาะสมที่สุดสำหรับเวลาตอบสนองฉันต้องการการรวมลูปซ้อนกันแบบซ้อนแทนที่จะรวมการผสาน DOP 1

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

DELETE ir
OUTPUT DELETED.id,  
    DELETED.VAL,  
    DELETED.RESET_VAL,  
    DELETED.GRP ,
    DELETED.initial_running_total - tb.running_total_to_subtract_this_loop
INTO #final_results
FROM #initial_results ir
INNER JOIN #group_bookkeeping tb ON ir.GRP = tb.GRP AND ir.ID <= tb.max_id_to_move
WHERE tb.grp_done = 0;

เพื่อความสะดวกของคุณด้านล่างคือรหัสเต็ม:

DECLARE @RC INT;
BEGIN
SET NOCOUNT ON;

CREATE TABLE #initial_results (
id int,
val int, 
reset_val int,
grp int,
initial_running_total int
);

CREATE TABLE #group_bookkeeping (
grp int,
max_id_to_move int,
running_total_to_subtract_this_loop int,
running_total_to_subtract_next_loop int,
grp_done bit, 
PRIMARY KEY (grp)
);

CREATE TABLE #final_results (
id int,
val int, 
reset_val int,
grp int,
running_total int
);

INSERT INTO #initial_results WITH (TABLOCK)
SELECT ID, VAL, RESET_VAL, GRP, SUM(VAL) OVER (PARTITION BY GRP ORDER BY ID) RUNNING_TOTAL
FROM reset_runn_total;

CREATE CLUSTERED INDEX i1 ON #initial_results (grp, id);

INSERT INTO #group_bookkeeping WITH (TABLOCK)
SELECT DISTINCT GRP, 0, 0, 0, 0
FROM reset_runn_total;

SET @RC = 1;
WHILE @RC > 0 
BEGIN
    WITH UPD_CTE AS (
        SELECT 
        #group_bookkeeping.GRP
        , MIN(CASE WHEN initial_running_total - #group_bookkeeping.running_total_to_subtract_next_loop > RESET_VAL THEN ID ELSE NULL END) max_id_to_move
        , MIN(#group_bookkeeping.running_total_to_subtract_next_loop) running_total_to_subtract_this_loop
        , MIN(CASE WHEN initial_running_total - #group_bookkeeping.running_total_to_subtract_next_loop > RESET_VAL THEN initial_running_total ELSE NULL END) additional_value_next_loop
        , CASE WHEN MIN(CASE WHEN initial_running_total - #group_bookkeeping.running_total_to_subtract_next_loop > RESET_VAL THEN ID ELSE NULL END) IS NULL THEN 1 ELSE 0 END grp_done
        FROM #group_bookkeeping 
        CROSS APPLY (SELECT ID, RESET_VAL, initial_running_total FROM #initial_results ir WHERE #group_bookkeeping.grp = ir.grp ) ir
        WHERE #group_bookkeeping.grp_done = 0
        GROUP BY #group_bookkeeping.GRP
    )
    UPDATE #group_bookkeeping
    SET #group_bookkeeping.max_id_to_move = uv.max_id_to_move
    , #group_bookkeeping.running_total_to_subtract_this_loop = uv.running_total_to_subtract_this_loop
    , #group_bookkeeping.running_total_to_subtract_next_loop = uv.additional_value_next_loop
    , #group_bookkeeping.grp_done = uv.grp_done
    FROM UPD_CTE uv
    WHERE uv.GRP = #group_bookkeeping.grp
    OPTION (LOOP JOIN);

    DELETE ir
    OUTPUT DELETED.id,  
        DELETED.VAL,  
        DELETED.RESET_VAL,  
        DELETED.GRP ,
        DELETED.initial_running_total - tb.running_total_to_subtract_this_loop
    INTO #final_results
    FROM #initial_results ir
    INNER JOIN #group_bookkeeping tb ON ir.GRP = tb.GRP AND ir.ID <= tb.max_id_to_move
    WHERE tb.grp_done = 0;

    SET @RC = @@ROWCOUNT;
END;

DELETE ir 
OUTPUT DELETED.id,  
    DELETED.VAL,  
    DELETED.RESET_VAL,  
    DELETED.GRP ,
    DELETED.initial_running_total - tb.running_total_to_subtract_this_loop
    INTO #final_results
FROM #initial_results ir
INNER JOIN #group_bookkeeping tb ON ir.GRP = tb.GRP;

CREATE CLUSTERED INDEX f1 ON #final_results (grp, id);

/* -- do something with the data
SELECT *
FROM #final_results
ORDER BY grp, id;
*/

DROP TABLE #final_results;
DROP TABLE #initial_results;
DROP TABLE #group_bookkeeping;

END;

ก็น่ากลัวฉันจะได้รับรางวัลคุณมีเงินรางวัล
P ரதீப்

ในเซิร์ฟเวอร์ของเราสำหรับ 50,000 grp และ 60 id ของคุณใช้เวลา 1 นาที 10 วินาที Recursive CTEใช้เวลา 2 นาที 15 วินาที
P ரதீப்

ฉันทดสอบรหัสทั้งสองด้วยข้อมูลเดียวกัน ของคุณยอดเยี่ยม มันสามารถปรับปรุงเพิ่มเติมได้หรือไม่
P ரதீப்

ฉันหมายถึงฉันวิ่งรหัสของคุณในข้อมูลจริงของเราและทดสอบ การคำนวณจะถูกประมวลผลในตารางอุณหภูมิในขั้นตอนจริงของฉันซึ่งส่วนใหญ่ควรจะมีการอัดแน่น มันจะดีถ้ามันสามารถลดให้เหลือประมาณ 30 วินาที
P ரதீப்

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

4

ใช้เคอร์เซอร์:

ALTER TABLE #reset_runn_total ADD RunningTotal int;

DECLARE @id int, @val int, @reset int, @acm int, @grp int, @last_grp int;
SET @acm = 0;

DECLARE curRes CURSOR FAST_FORWARD FOR 
SELECT id, val, reset_val, grp
FROM #reset_runn_total
ORDER BY grp, id;

OPEN curRes;
FETCH NEXT FROM curRes INTO @id, @val, @reset, @grp;
SET @last_grp = @grp;

WHILE @@FETCH_STATUS = 0  
BEGIN
    IF @grp <> @last_grp SET @acm = 0;
    SET @last_grp = @grp;
    SET @acm = @acm + @val;
    UPDATE #reset_runn_total
    SET RunningTotal = @acm
    WHERE id = @id;
    IF @acm > @reset SET @acm = 0;
    FETCH NEXT FROM curRes INTO @id, @val, @reset, @grp;
END

CLOSE curRes;
DEALLOCATE curRes;

+----+-----+-----------+-------------+
| id | val | reset_val | RunningTotal|
+----+-----+-----------+-------------+
| 1  | 1   | 10        |     1       |
+----+-----+-----------+-------------+
| 2  | 8   | 12        |     9       |
+----+-----+-----------+-------------+
| 3  | 6   | 14        |     15      |
+----+-----+-----------+-------------+
| 4  | 5   | 10        |     5       |
+----+-----+-----------+-------------+
| 5  | 6   | 13        |     11      |
+----+-----+-----------+-------------+
| 6  | 3   | 11        |     14      |
+----+-----+-----------+-------------+
| 7  | 9   | 8         |     9       |
+----+-----+-----------+-------------+
| 8  | 10  | 12        |     10      |
+----+-----+-----------+-------------+

ตรวจสอบที่นี่: http://rextester.com/WSPLO95303


3

ไม่ได้เรียงอยู่ แต่เป็นเวอร์ชัน SQL ที่แท้จริง:

WITH x AS (
    SELECT TOP 1 id,
           val,
           reset_val,
           val AS running_total,
           1 AS level 
      FROM reset_runn_total
    UNION ALL
    SELECT r.id,
           r.val,
           r.reset_val,
           CASE WHEN x.running_total < x.reset_val THEN x.running_total + r.val ELSE r.val END,
           level = level + 1
      FROM x JOIN reset_runn_total AS r ON (r.id > x.id)
) SELECT
  *
FROM x
WHERE NOT EXISTS (
        SELECT 1
        FROM x AS x2
        WHERE x2.id = x.id
        AND x2.level > x.level
    )
ORDER BY id, level DESC
;

ฉันไม่ใช่ผู้เชี่ยวชาญในภาษาของ SQL Server นี่เป็นรุ่นเริ่มต้นสำหรับ PostrgreSQL (ถ้าฉันเข้าใจถูกต้องฉันไม่สามารถใช้ LIMIT 1 / TOP 1 ในส่วนที่เรียกซ้ำใน SQL Server):

WITH RECURSIVE x AS (
    (SELECT id, val, reset_val, val AS running_total
       FROM reset_runn_total
      ORDER BY id
      LIMIT 1)
    UNION
    (SELECT r.id, r.val, r.reset_val,
            CASE WHEN x.running_total < x.reset_val THEN x.running_total + r.val ELSE r.val END
       FROM x JOIN reset_runn_total AS r ON (r.id > x.id)
      ORDER BY id
      LIMIT 1)
) SELECT * FROM x;

@ JoeObbish บอกตามตรงว่ามันไม่ชัดเจนเลยจากคำถาม ผลลัพธ์ที่ต้องการเช่นไม่แสดงgrpคอลัมน์
ypercubeᵀᴹ

@ JoeObbish นั่นคือสิ่งที่ฉันเข้าใจเช่นกัน แต่คำถามอาจได้รับประโยชน์จากคำแถลงที่ชัดเจนเกี่ยวกับเรื่องนั้น รหัสในคำถาม (กับ CTE) ไม่ได้ใช้มันอย่างใดอย่างหนึ่ง (และมันยังมีคอลัมน์ชื่อแตกต่างกัน) จะเห็นได้ชัดว่าใครก็ตามที่อ่านคำถาม - พวกเขาจะไม่ - และไม่ควร - ต้องอ่านคำตอบหรือความคิดเห็นอื่น ๆ
ypercubeᵀᴹ

@ ypercubeᵀᴹเพิ่มข้อมูลที่จำเป็นสำหรับคำถาม
P ரதீப்

1

ดูเหมือนว่าคุณมีคำถาม / วิธีการหลายวิธีในการโจมตีปัญหา แต่คุณยังไม่ได้ให้เรา - หรือพิจารณาด้วย? - ดัชนีบนโต๊ะ

ดัชนีอะไรอยู่ในตาราง? มันเป็นฮีปหรือมีดัชนีคลัสเตอร์หรือไม่?

ฉันจะลองวิธีแก้ไขปัญหาต่าง ๆ ที่แนะนำหลังจากเพิ่มดัชนีนี้:

(grp, id) INCLUDE (val, reset_val)

หรือเพียงแค่การเปลี่ยนแปลง (หรือ) (grp, id)ดัชนีคลัสเตอร์จะเป็น

การมีดัชนีที่ตั้งเป้าหมายการสืบค้นเฉพาะควรปรับปรุงประสิทธิภาพ - ส่วนใหญ่ถ้าไม่ใช่วิธีการทั้งหมด


เพิ่มข้อมูลที่จำเป็นสำหรับคำถาม
P ரதீப்
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.