ใน SQL Server มีวิธีการตรวจสอบว่ากลุ่มแถวที่เลือกถูกล็อคหรือไม่?


21

เราพยายามที่จะปรับปรุง / ลบบันทึกจำนวนมากในตารางแถวหลายพันล้าน เนื่องจากนี่เป็นตารางยอดนิยมจึงมีกิจกรรมมากมายในส่วนต่างๆของตารางนี้ กิจกรรมการอัปเดต / ลบขนาดใหญ่ใด ๆ จะถูกปิดกั้นเป็นระยะเวลานาน (เนื่องจากกำลังรอการล็อคในแถวหรือการล็อกหน้าหรือการล็อคตาราง) ทำให้หมดเวลาหรือใช้เวลาหลายวันเพื่อทำงานให้เสร็จ

ดังนั้นเรากำลังเปลี่ยนวิธีการลบแถวเล็ก ๆ ในเวลาเดียวกัน แต่เราต้องการตรวจสอบว่าแถวที่เลือก (สมมติว่า 100 หรือ 1,000 หรือ 2,000 แถว) ปัจจุบันถูกล็อคโดยกระบวนการที่แตกต่างกันหรือไม่

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

เป็นไปได้หรือไม่

ขอบคุณ ToC


2
คุณได้ดู READPAST เป็นส่วนหนึ่งของคำสั่งลบหรือ NOWAIT (ทำให้กลุ่มทั้งหมดล้มเหลว) หรือไม่ หนึ่งในนั้นอาจใช้ได้ผลกับคุณ msdn.microsoft.com/en-us/library/ms187373.aspx
Sean กล่าวว่าเอา Sara Chipps ออก

@ SeanGallardy ฉันไม่ได้พิจารณาความคิดนั้น แต่ตอนนี้ฉันจะ แต่มีวิธีที่ง่ายกว่าในการตรวจสอบว่าแถวใดแถวหนึ่งถูกล็อกหรือไม่? ขอบคุณ
ToC

3
คุณอาจดูเป็น LOCK_TIMEOUT ( msdn.microsoft.com/en-us/library/ms189470.aspx ) ตัวอย่างเช่นนี่คือวิธีที่ sp_whoisactive ของ Adam Machanic ทำให้กระบวนการไม่รอนานเกินไปหากถูกบล็อกเมื่อพยายามรวบรวมแผนการดำเนินการ คุณสามารถตั้งค่าการหมดเวลาสั้น ๆ หรือแม้กระทั่งใช้ค่า 0 ("0 หมายถึงไม่ต้องรอเลยและกลับข้อความทันทีที่พบล็อค") คุณสามารถรวมสิ่งนี้กับ TRY / CATCH เพื่อตรวจจับข้อผิดพลาด 1222 ( "เกินกำหนดเวลาล็อคคำขอออก") และดำเนินการต่อในชุดถัดไป
Geoff Patterson

@gpatterson วิธีการที่น่าสนใจ ฉันจะลองด้วย
ToC

2
ตอบได้เลยไม่มีวิธีที่ง่ายกว่าในการดูว่าแถวถูกล็อคหรือไม่เว้นแต่มีบางสิ่งที่ทำโดยเฉพาะในแอปพลิเคชัน โดยทั่วไปคุณสามารถทำการเลือกด้วย HOLDLOCK และ XLOCK ด้วยชุด lock_timeout (ซึ่งเป็นสิ่งที่ NOWAIT ในความคิดเห็นดั้งเดิมของฉันเกี่ยวกับการตั้งค่าการหมดเวลาเป็น 0) หากคุณไม่ได้รับมันแสดงว่าคุณรู้ว่ามีบางอย่างถูกล็อค ไม่มีอะไรจะพูดง่ายๆ ว่า "แถว X ในตาราง Y โดยใช้ดัชนี Z ถูกล็อกโดยบางสิ่ง" เราสามารถดูว่าตารางมีการล็อกหรือหน้า / แถว / คีย์ / ฯลฯ มีการล็อกหรือไม่ แต่การแปลให้เป็นแถวที่ระบุในแบบสอบถามจะไม่ใช่เรื่องง่าย
ฌอนกล่าวว่าลบ Sara Chipps

คำตอบ:


10

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

ตามที่ได้กล่าวมาแล้วมันมีเหตุผลที่จะใช้คำแนะนำ READPAST และระดับการแยก READPETTED (ค่าเริ่มต้น) เพื่อข้ามช่วงที่ผ่านมาซึ่งอาจมีแถวที่ถูกบล็อก ฉันจะไปอีกขั้นหนึ่งและแนะนำให้ใช้ระดับการแยกแบบ SERIALIZABLE และการลบแบบแทะ

SQL Server ใช้การล็อก Key-Range เพื่อปกป้องช่วงของแถวที่รวมอยู่ในชุดระเบียนที่ถูกอ่านโดยคำสั่ง Transact-SQL ในขณะที่ใช้ระดับการแยกธุรกรรมที่สามารถทำให้เป็นลำดับได้ ... หาข้อมูลเพิ่มเติมได้ที่นี่: https://technet.microsoft.com /en-US/library/ms191272(v=SQL.105).aspx

ด้วยการลบ nibbling เป้าหมายของเราคือการแยกช่วงของแถวและตรวจสอบให้แน่ใจว่าไม่มีการเปลี่ยนแปลงเกิดขึ้นกับแถวเหล่านั้นในขณะที่เรากำลังลบพวกเขากล่าวคือเราไม่ต้องการอ่านหรือแทรก phantom ระดับการแยกแบบอนุกรมได้หมายถึงการแก้ปัญหานี้

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

หมายเหตุเกี่ยวกับการดูแลรักษาบ้าน:

  1. SQL Server รุ่นที่ฉันใช้คือ Microsoft SQL Server 2012 - 11.0.5343.0 (X64)
  2. ฐานข้อมูลทดสอบของฉันใช้โมเดลการกู้คืนแบบเต็ม

ในการเริ่มต้นการทดสอบของฉันฉันจะตั้งค่าฐานข้อมูลทดสอบตารางตัวอย่างและฉันจะเติมตารางด้วย 2,000,000 แถว


USE [master];
GO

SET NOCOUNT ON;

IF DATABASEPROPERTYEX (N'test', N'Version') > 0
BEGIN
    ALTER DATABASE [test] SET SINGLE_USER
        WITH ROLLBACK IMMEDIATE;
    DROP DATABASE [test];
END
GO

-- Create the test database
CREATE DATABASE [test];
GO

-- Set the recovery model to FULL
ALTER DATABASE [test] SET RECOVERY FULL;

-- Create a FULL database backup
-- in order to ensure we are in fact using 
-- the FULL recovery model
-- I pipe it to dev null for simplicity
BACKUP DATABASE [test]
TO DISK = N'nul';
GO

USE [test];
GO

-- Create our table
IF OBJECT_ID('dbo.tbl','U') IS NOT NULL
BEGIN
    DROP TABLE dbo.tbl;
END;
CREATE TABLE dbo.tbl
(
      c1 BIGINT IDENTITY (1,1) NOT NULL
    , c2 INT NOT NULL
) ON [PRIMARY];
GO

-- Insert 2,000,000 rows 
INSERT INTO dbo.tbl
    SELECT TOP 2000
        number
    FROM
        master..spt_values
    ORDER BY 
        number
GO 1000

ณ จุดนี้เราจะต้องมีดัชนีตั้งแต่หนึ่งตัวขึ้นไปซึ่งกลไกการล็อคของระดับการแยกแบบ SERIALIZABLE สามารถกระทำได้


-- Add a clustered index
CREATE UNIQUE CLUSTERED INDEX CIX_tbl_c1
    ON dbo.tbl (c1);
GO

-- Add a non-clustered index
CREATE NONCLUSTERED INDEX IX_tbl_c2 
    ON dbo.tbl (c2);
GO

ตอนนี้ให้เราตรวจสอบเพื่อดูว่ามีการสร้าง 2,000,000 แถวของเรา


SELECT
    COUNT(*)
FROM
    tbl;

ป้อนคำอธิบายรูปภาพที่นี่

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


DECLARE
      @BatchSize        INT    = 100
    , @LowestValue      BIGINT = 20000
    , @HighestValue     BIGINT = 20010
    , @DeletedRowsCount BIGINT = 0
    , @RowCount         BIGINT = 1;

SET NOCOUNT ON;
GO

WHILE  @DeletedRowsCount <  ( @HighestValue - @LowestValue ) 
BEGIN

    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
    BEGIN TRANSACTION

        DELETE 
        FROM
            dbo.tbl 
        WHERE
            c1 IN ( 
                    SELECT TOP (@BatchSize)
                        c1
                    FROM
                        dbo.tbl 
                    WHERE 
                        c1 BETWEEN @LowestValue AND @HighestValue
                    ORDER BY 
                        c1
                  );

        SET @RowCount = ROWCOUNT_BIG();

    COMMIT TRANSACTION;

    SET @DeletedRowsCount += @RowCount;
    WAITFOR DELAY '000:00:00.025';
    CHECKPOINT;

END;

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

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

ตอนนี้ฉันต้องการให้ตัวอย่างสั้น ๆ ของรูทีนการลบที่ใช้งานจริง เราต้องเปิดหน้าต่างใหม่ภายใน SSMS และลบหนึ่งแถวออกจากตารางของเรา ฉันจะทำสิ่งนี้ภายในธุรกรรมโดยนัยโดยใช้ระดับการแยกเริ่มต้นของ READ COMMITTED


DELETE FROM
    dbo.tbl
WHERE
    c1 = 20005;

แถวนี้ถูกลบจริงหรือไม่?


SELECT
    c1
FROM
    dbo.tbl
WHERE
    c1 BETWEEN 20000 AND 20010;

ใช่มันถูกลบไปแล้ว

หลักฐานการลบแถว

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


SELECT
    DB_NAME(resource_database_id) AS DatabaseName
  , resource_type
  , request_mode
FROM
    sys.dm_tran_locks
WHERE
    DB_NAME(resource_database_id) = 'test'
    AND resource_type = 'KEY'
ORDER BY
    request_mode;

-- Our insert
sp_lock 55;

-- Our deletions
sp_lock 52;

-- Our active sessions
sp_whoisactive;

ตอนนี้เราพร้อมที่จะเริ่ม ในหน้าต่าง SSMS ใหม่ให้เราเริ่มทำธุรกรรมอย่างชัดเจนซึ่งจะพยายามแทรกแถวที่เราลบอีกครั้ง ในเวลาเดียวกันเราจะทำการปิดการดำเนินการลบแทปของเรา

รหัสแทรก:


BEGIN TRANSACTION

    SET IDENTITY_INSERT dbo.tbl ON;

    INSERT  INTO dbo.tbl
            ( c1 , c2 )
    VALUES
            ( 20005 , 1 );

    SET IDENTITY_INSERT dbo.tbl OFF;

--COMMIT TRANSACTION;

ให้เราเริ่มการดำเนินการทั้งสองเริ่มด้วยการแทรกและตามด้วยการลบของเรา เราสามารถเห็นกุญแจล็อคช่วงและล็อคพิเศษ

Range และ eXclusive Locks

ส่วนแทรกสร้างล็อคเหล่านี้:

ล็อคของแทรก

การลบ / เลือก nibbling ถือล็อคเหล่านี้:

ป้อนคำอธิบายรูปภาพที่นี่

ส่วนแทรกของเราบล็อกการลบของเราตามที่คาดไว้:

แทรกการลบบล็อก

ตอนนี้ให้เรากระทำธุรกรรมแทรกและดูว่ามีอะไรเกิดขึ้น

กระทำการลบ

และตามที่คาดไว้การทำธุรกรรมทั้งหมดเสร็จสมบูรณ์ ตอนนี้เราต้องตรวจสอบเพื่อดูว่าส่วนแทรกนั้นเป็นภาพหลอนหรือไม่หรือการลบลบออกเช่นกัน


SELECT
    c1
FROM
    dbo.tbl
WHERE
    c1 BETWEEN 20000 AND 20015;

อันที่จริงการแทรกถูกลบ; ดังนั้นจึงไม่อนุญาตการแทรกภาพหลอน

ไม่มีการแทรกภาพหลอน

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

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

โปรดแจ้งให้เราทราบว่าคุณคิดอย่างไร

ฉันสร้างตัวอย่างเพิ่มเติมของระดับการแยกแบบ SERIALIZABLE ในการดำเนินการ พวกเขาควรจะอยู่ที่ลิงค์ด้านล่าง

ลบการทำงาน

แทรกการดำเนินงาน

การดำเนินการด้านความเท่าเทียมกัน - ล็อคช่วงคีย์บนค่าคีย์ถัดไป

Equality Operations - Singleton Fetch ของข้อมูลที่มีอยู่

Equality Operations - Singleton Fetch ของข้อมูลที่ไม่มีอยู่

การดำเนินการไม่เท่าเทียมกัน - ล็อคช่วง Key-Range และค่าคีย์ถัดไป


9

ดังนั้นเรากำลังเปลี่ยนวิธีการลบแถวเล็ก ๆ ในเวลาเดียวกัน

นี้เป็นความคิดที่ดีมากที่จะลบในbatches ระวังขนาดเล็กหรือชิ้น ฉันจะเพิ่มขนาดเล็กwaitfor delay '00:00:05'และขึ้นอยู่กับรูปแบบการกู้คืนของฐานข้อมูล - ถ้าFULLทำแล้วlog backupและถ้าSIMPLEทำmanual CHECKPOINTเพื่อหลีกเลี่ยงการ bloating ของบันทึกธุรกรรม - ระหว่างแบตช์

แต่เราต้องการตรวจสอบว่าแถวที่เลือก (สมมติว่า 100 หรือ 1,000 หรือ 2,000 แถว) ปัจจุบันถูกล็อคโดยกระบวนการที่แตกต่างกันหรือไม่

สิ่งที่คุณกำลังบอกไม่เป็นไปได้ทั้งหมดออกจากกล่อง (คำนึงถึง 3 สัญลักษณ์หัวข้อย่อยของคุณ) หากข้อเสนอแนะดังกล่าวข้างต้น - small batches + waitfor delayไม่ทำงาน (ให้คุณทำทดสอบที่เหมาะสม) query HINTแล้วคุณสามารถใช้

อย่าใช้NOLOCK- ดูกิโลไบต์ / 308886 , SQL Server อ่านอย่างสม่ำเสมอปัญหาโดย Itzik เบนกาน , วาง NOLOCK ทุกที่ - โดยแอรอนเบอร์ทรานด์และSQL Server NOLOCK คำแนะนำและอื่น ๆ ความคิดที่ไม่ดี

READPASTคำใบ้จะช่วยในสถานการณ์ของคุณ ส่วนสำคัญของREADPASTคำใบ้คือ - หากมีการล็อคระดับแถวเซิร์ฟเวอร์ SQL จะไม่อ่าน

ระบุว่า Database Engine ไม่อ่านแถวที่ถูกล็อกโดยธุรกรรมอื่น เมื่อREADPASTระบุไว้การล็อคระดับแถวจะถูกข้ามไป นั่นคือโปรแกรมฐานข้อมูลข้ามไปที่แถวแทนที่จะบล็อกธุรกรรมปัจจุบันจนกว่าการล็อกจะถูกนำออกใช้

ในระหว่างการทดสอบที่ จำกัด ของฉันฉันพบปริมาณงานที่ดีจริง ๆ เมื่อใช้DELETE from schema.tableName with (READPAST, READCOMMITTEDLOCK)และตั้งค่าระดับการแยกเซสชั่นแบบสอบถามเป็นการREAD COMMITTEDใช้SET TRANSACTION ISOLATION LEVEL READ COMMITTEDซึ่งเป็นระดับการแยกเริ่มต้นอยู่แล้ว


2

การสรุปวิธีการอื่นที่เสนอไว้ในความคิดเห็นของคำถาม


  1. ใช้NOWAITหากพฤติกรรมที่ต้องการคือทำให้ก้อนทั้งหมดล้มเหลวทันทีที่พบการล็อคที่เข้ากันไม่ได้

    จากNOWAITเอกสาร :

    สั่งให้ Database Engine ส่งคืนข้อความทันทีที่พบการล็อคบนตาราง NOWAITเทียบเท่ากับการระบุSET LOCK_TIMEOUT 0สำหรับตารางที่ระบุ NOWAITคำใบ้ไม่ทำงานเมื่อTABLOCKคำแนะนำรวมทั้งยังเป็น หากต้องการยกเลิกแบบสอบถามโดยไม่รอเมื่อใช้TABLOCKคำใบ้ให้ใส่คำนำหน้าด้วยSETLOCK_TIMEOUT 0;แทน

  2. ใช้SET LOCK_TIMEOUTเพื่อให้ได้ผลลัพธ์ที่คล้ายกัน แต่ด้วยการหมดเวลาที่กำหนดได้:

    จากSET LOCK_TIMEOUTเอกสารประกอบ

    ระบุจำนวนมิลลิวินาทีที่คำสั่งรอให้การล็อกถูกปล่อย

    เมื่อการรอการล็อกเกินกว่าค่าการหมดเวลาข้อผิดพลาดจะถูกส่งกลับ ค่า 0 หมายถึงไม่ต้องรอเลยและส่งคืนข้อความทันทีที่พบการล็อค


0

ในการสมมติว่าเรามี 2 ข้อความค้นหาคล้ายคลึงกัน:

เชื่อมต่อ / เซสชั่น 1: จะล็อคแถว = 777

SELECT * FROM your_table WITH(UPDLOCK,READPAST) WHERE id = 777

เชื่อมต่อ / เซสชั่น 2: จะไม่สนใจแถวล็อค = 777

SELECT * FROM your_table WITH(UPDLOCK,READPAST) WHERE id = 777

หรือเชื่อมต่อ / เซสชัน 2: จะมีข้อยกเว้น

DECLARE @id integer;
SELECT @id = id FROM your_table WITH(UPDLOCK,READPAST) WHERE id = 777;
IF @id is NULL
  THROW 51000, 'Hi, a record is locked or does not exist.', 1;

-1

ลองกรองบางอย่างเช่นนี้ - มันอาจซับซ้อนหากคุณต้องการได้รับเฉพาะเจาะจงจริงๆ ค้นหา BOL สำหรับคำอธิบายของsys.dm_tran_locks

SELECT 
tl.request_session_id,
tl.resource_type,
tl.resource_associated_entity_id,
db_name(tl.resource_database_id) 'Database',
CASE 
    WHEN tl.resource_type = 'object' THEN object_name(tl.resource_associated_entity_id, tl.resource_database_id)
    ELSE NULL
END 'LockedObject',
tl.resource_database_id,
tl.resource_description,
tl.request_mode,
tl.request_type,
tl.request_status FROM [sys].[dm_tran_locks] tl WHERE resource_database_id <> 2order by tl.request_session_id

แค่อยากรู้อยากเห็น - ทำไมการลงคะแนน?
rottengeek

-11

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

DELETE TA FROM dbo.TableA TA WITH (NOLOCK) WHERE Condition = True

7
ถ้าฉันพยายามที่ในเครื่องของฉันฉันได้รับในท้องถิ่นMsg 1065, Level 15, State 1, Line 15 The NOLOCK and READUNCOMMITTED lock hints are not allowed for target tables of INSERT, UPDATE, DELETE or MERGE statements., เลิกใช้มาตั้งแต่ปี 2005
ทอม V - ทีมโมนิกา
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.