การปรับประสิทธิภาพในแบบสอบถาม


9

การค้นหาความช่วยเหลือเพื่อปรับปรุงประสิทธิภาพการสืบค้นนี้

SQL Server 2008 R2 Enterprise , RAM สูงสุด 16 GB, CPU 40, Max Parallelism 4

SELECT DsJobStat.JobName AS JobName
    , AJF.ApplGroup AS GroupName
    , DsJobStat.JobStatus AS JobStatus
    , AVG(CAST(DsJobStat.ElapsedSec AS FLOAT)) AS ElapsedSecAVG
    , AVG(CAST(DsJobStat.CpuMSec AS FLOAT)) AS CpuMSecAVG 
FROM DsJobStat, AJF 
WHERE DsJobStat.NumericOrderNo=AJF.OrderNo 
AND DsJobStat.Odate=AJF.Odate 
AND DsJobStat.JobName NOT IN( SELECT [DsAvg].JobName FROM [DsAvg] )         
GROUP BY DsJobStat.JobName
, AJF.ApplGroup
, DsJobStat.JobStatus
HAVING AVG(CAST(DsJobStat.ElapsedSec AS FLOAT)) <> 0;

ข้อความดำเนินการ

(0 row(s) affected)
Table 'AJF'. Scan count 11, logical reads 45, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'DsAvg'. Scan count 2, logical reads 1926, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'DsJobStat'. Scan count 1, logical reads 3831235, physical reads 85, read-ahead reads 3724396, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

(1 row(s) affected)

SQL Server Execution Times:
      CPU time = 67268 ms,  elapsed time = 90206 ms.

โครงสร้างของตาราง:

-- 212271023 rows
CREATE TABLE [dbo].[DsJobStat](
    [OrderID] [nvarchar](8) NOT NULL,
    [JobNo] [int] NOT NULL,
    [Odate] [datetime] NOT NULL,
    [TaskType] [nvarchar](255) NULL,
    [JobName] [nvarchar](255) NOT NULL,
    [StartTime] [datetime] NULL,
    [EndTime] [datetime] NULL,
    [NodeID] [nvarchar](255) NULL,
    [GroupName] [nvarchar](255) NULL,
    [CompStat] [int] NULL,
    [RerunCounter] [int] NOT NULL,
    [JobStatus] [nvarchar](255) NULL,
    [CpuMSec] [int] NULL,
    [ElapsedSec] [int] NULL,
    [StatusReason] [nvarchar](255) NULL,
    [NumericOrderNo] [int] NULL,
CONSTRAINT [PK_DsJobStat] PRIMARY KEY CLUSTERED 
(   [OrderID] ASC,
    [JobNo] ASC,
    [Odate] ASC,
    [JobName] ASC,
    [RerunCounter] ASC
));

-- 48992126 rows
CREATE TABLE [dbo].[AJF](  
    [JobName] [nvarchar](255) NOT NULL,
    [JobNo] [int] NOT NULL,
    [OrderNo] [int] NOT NULL,
    [Odate] [datetime] NOT NULL,
    [SchedTab] [nvarchar](255) NULL,
    [Application] [nvarchar](255) NULL,
    [ApplGroup] [nvarchar](255) NULL,
    [GroupName] [nvarchar](255) NULL,
    [NodeID] [nvarchar](255) NULL,
    [Memlib] [nvarchar](255) NULL,
    [Memname] [nvarchar](255) NULL,
    [CreationTime] [datetime] NULL,
CONSTRAINT [AJF$PrimaryKey] PRIMARY KEY CLUSTERED 
(   [JobName] ASC,
    [JobNo] ASC,
    [OrderNo] ASC,
    [Odate] ASC
));

-- 413176 rows
CREATE TABLE [dbo].[DsAvg](
    [JobName] [nvarchar](255) NULL,
    [GroupName] [nvarchar](255) NULL,
    [JobStatus] [nvarchar](255) NULL,
    [ElapsedSecAVG] [float] NULL,
    [CpuMSecAVG] [float] NULL
);

CREATE NONCLUSTERED INDEX [DJS_Dashboard_2] ON [dbo].[DsJobStat] 
(   [JobName] ASC,
    [Odate] ASC,
    [StartTime] ASC,
    [EndTime] ASC
)
INCLUDE ( [OrderID],
[JobNo],
[NodeID],
[GroupName],
[JobStatus],
[CpuMSec],
[ElapsedSec],
[NumericOrderNo]) ;

CREATE NONCLUSTERED INDEX [Idx_Dashboard_AJF] ON [dbo].[AJF] 
(   [OrderNo] ASC,
[Odate] ASC
)
INCLUDE ( [SchedTab],
[Application],
[ApplGroup]) ;

CREATE NONCLUSTERED INDEX [DsAvg$JobName] ON [dbo].[DsAvg] 
(   [JobName] ASC
)

แผนการดำเนินการ:

https://www.brentozar.com/pastetheplan/?id=rkUVhMlXM


อัปเดตหลังจากได้รับคำตอบ

ขอบคุณมาก @Joe Obbish

คุณพูดถูกเกี่ยวกับคำถามนี้ซึ่งอยู่ระหว่าง DsJobStat และ DsAvg มันไม่มากเกี่ยวกับวิธีการเข้าร่วมและไม่ได้ใช้ไม่ได้

แน่นอนมีโต๊ะตามที่คุณเดา

CREATE TABLE [dbo].[DSJobNames](
    [JobName] [nvarchar](255) NOT NULL,
 CONSTRAINT [DSJobNames$PrimaryKey] PRIMARY KEY CLUSTERED 
(   [JobName] ASC
) ); 

ฉันลองคำแนะนำของคุณ

SELECT DsJobStat.JobName AS JobName
, AJF.ApplGroup AS GroupName
, DsJobStat.JobStatus AS JobStatus
, AVG(CAST(DsJobStat.ElapsedSec AS FLOAT)) AS ElapsedSecAVG
, Avg(CAST(DsJobStat.CpuMSec AS FLOAT)) AS CpuMSecAVG 
FROM DsJobStat
INNER JOIN DSJobNames jn
    ON jn.[JobName]= DsJobStat.[JobName]
INNER JOIN AJF 
    ON DsJobStat.Odate=AJF.Odate 
    AND DsJobStat.NumericOrderNo=AJF.OrderNo 
WHERE NOT EXISTS ( SELECT 1 FROM [DsAvg] WHERE jn.JobName =  [DsAvg].JobName )      
GROUP BY DsJobStat.JobName, AJF.ApplGroup, DsJobStat.JobStatus
HAVING AVG(CAST(DsJobStat.ElapsedSec AS FLOAT)) <> 0;   

ข้อความดำเนินการ:

(0 row(s) affected)
Table 'DSJobNames'. Scan count 5, logical reads 1244, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'DsAvg'. Scan count 5, logical reads 2129, physical reads 0, read-ahead reads 24, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'DsJobStat'. Scan count 8, logical reads 84, physical reads 0, read-ahead reads 83, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'AJF'. Scan count 5, logical reads 757999, physical reads 944, read-ahead reads 757311, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

(1 row(s) affected)

 SQL Server Execution Times:
   CPU time = 21776 ms,  elapsed time = 33984 ms.

แผนการดำเนินการ: https://www.brentozar.com/pastetheplan/?id=rJVkLSZ7f


หากเป็นรหัสผู้ขายที่คุณไม่สามารถเปลี่ยนแปลงได้สิ่งที่ดีที่สุดที่ต้องทำคือเปิดเหตุการณ์การสนับสนุนกับผู้ขายซึ่งเจ็บปวดอย่างที่ควรจะเป็นและเอาชนะพวกเขาด้วยการมีคิวรีที่ต้องอ่านให้มาก ประโยค NOT IN ที่อ้างถึงค่าในตารางที่มี 413,000 แถวคือ, เอ่อ, ดีที่สุดย่อย การสแกนดัชนีใน DSJobStat กำลังส่งคืน 212 ล้านแถวซึ่งมีฟองซ้อนกันถึง 212 ล้านลูปซ้อนกันและคุณสามารถเห็นจำนวน 212 ล้านแถวเป็น 83% ของต้นทุน ผมไม่คิดว่าคุณสามารถช่วยนี้โดยไม่ได้เขียนคำถามหรือกวาดล้างข้อมูล ...
โทนี่ฮิงเกิ้ล

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

คำตอบ:


11

เริ่มต้นด้วยการพิจารณาคำสั่งเข้าร่วม คุณมีการอ้างอิงสามตารางในแบบสอบถาม คำสั่งซื้อที่เข้าร่วมชุดใดที่ให้ประสิทธิภาพที่ดีที่สุด เครื่องมือเพิ่มประสิทธิภาพการสืบค้นคิดว่าการเข้าร่วมจากDsJobStatถึงDsAvgจะกำจัดแถวเกือบทั้งหมด (การประเมินความผิดพลาดเชิงลดลงจาก 212195000 เป็น 1 แถว) แผนจริงแสดงให้เราเห็นว่าการประมาณการนั้นใกล้เคียงกับความเป็นจริงมาก (11 แถวรอดจากการเข้าร่วม) อย่างไรก็ตามการเข้าร่วมนั้นดำเนินการเป็นการป้องกันการรวมกึ่งผสานที่ถูกต้องดังนั้นการDsJobStatสแกนทั้งหมด 212 ล้านแถวจากตารางจะถูกสแกนเพื่อสร้าง 11 แถว แน่นอนว่าอาจเป็นการมีส่วนร่วมในการดำเนินการกับแบบสอบถามแบบยาว แต่ฉันไม่สามารถนึกถึงตัวดำเนินการทางกายภาพหรือตรรกะที่ดีกว่าสำหรับการเข้าร่วมที่จะดีกว่า ฉันแน่ใจว่าDJS_Dashboard_2ดัชนีใช้สำหรับคิวรีอื่น ๆ แต่คีย์พิเศษทั้งหมดและคอลัมน์ที่รวมไว้จะต้องใช้ IO เพิ่มขึ้นสำหรับเคียวรีนี้และทำให้คุณช้าลง ดังนั้นคุณอาจมีปัญหาการเข้าถึงตารางด้วยการสแกนดัชนีบนDsJobStatตาราง

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

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

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

CREATE TABLE [dbo].[DsAvg](
    [JobName] [nvarchar](255) NULL
);

CREATE CLUSTERED INDEX CI_DsAvg ON [DsAvg] (JobName);

INSERT INTO [DsAvg] WITH (TABLOCK)
SELECT TOP (200000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

CREATE TABLE [dbo].[DsJobStat](
    [JobName] [nvarchar](255) NOT NULL,
    [JobStatus] [nvarchar](255) NULL,
);

CREATE CLUSTERED INDEX CI_JobStat ON DsJobStat (JobName)

INSERT INTO [DsJobStat] WITH (TABLOCK)
SELECT [JobName], 'ACTIVE'
FROM [DsAvg] ds
CROSS JOIN (
SELECT TOP (1000) 1
FROM master..spt_values t1
) c (t);

INSERT INTO [DsJobStat] WITH (TABLOCK)
SELECT TOP (1000) '200001', 'ACTIVE'
FROM master..spt_values t1;

จากแผนแบบสอบถามเราจะเห็นว่ามีJobNameค่าที่ไม่ซ้ำกันประมาณ 200,000 รายการในDsAvgตาราง ขึ้นอยู่กับจำนวนแถวจริงหลังจากการเข้าร่วมกับตารางนั้นเราจะเห็นได้ว่าค่าเกือบทั้งหมดJobNameในDsJobStatนั้นอยู่ในDsAvgตารางด้วยเช่นกัน ดังนั้นDsJobStatตารางมีค่าที่ไม่ซ้ำกัน 200001 สำหรับJobNameคอลัมน์และ 1,000 แถวต่อค่า

ฉันเชื่อว่าข้อความค้นหานี้แสดงถึงปัญหาประสิทธิภาพ:

SELECT DsJobStat.JobName AS JobName, DsJobStat.JobStatus AS JobStatus
FROM DsJobStat
WHERE DsJobStat.JobName NOT IN( SELECT [DsAvg].JobName FROM [DsAvg] );

สิ่งอื่น ๆ ทั้งหมดในแผนคิวรีของคุณ ( GROUP BY,, การHAVINGรวมสไตล์โบราณ ฯลฯ ) เกิดขึ้นหลังจากชุดผลลัพธ์ถูกลดเหลือ 11 แถว ขณะนี้ไม่สำคัญจากมุมมองประสิทธิภาพการค้นหา แต่อาจมีข้อกังวลอื่น ๆ ที่อาจเปิดเผยโดยข้อมูลที่เปลี่ยนแปลงในตารางของคุณ

ฉันกำลังทดสอบใน SQL Server 2017 แต่ฉันได้รับรูปร่างพื้นฐานแบบเดียวกับคุณ:

ก่อนวางแผน

บนเครื่องของฉันเคียวรีนั้นใช้เวลา CPU 62219 มิลลิวินาทีและเวลาผ่านไป 65576 มิลลิวินาทีเพื่อดำเนินการ ถ้าฉันเขียนแบบสอบถามที่จะใช้NOT EXISTS:

SELECT DsJobStat.JobName AS JobName, DsJobStat.JobStatus AS JobStatus
FROM DsJobStat
WHERE NOT EXISTS (SELECT 1 FROM [DsAvg] WHERE DsJobStat.JobName = [DsAvg].JobName);

ไม่มีสปูล

สปูลนั้นไม่ถูกเรียกใช้งานอีก 212 ล้านครั้งและอาจมีพฤติกรรมที่ตั้งใจจากผู้ขาย ตอนนี้แบบสอบถามดำเนินการใน 34516 ms ของเวลา CPU และ 41132 ms ของเวลาที่ผ่านไป เวลาส่วนใหญ่ใช้เวลาสแกน 212 ล้านแถวจากดัชนี

การสแกนดัชนีนั้นโชคร้ายมากสำหรับแบบสอบถามนั้น โดยเฉลี่ยแล้วเรามี 1,000 แถวต่อค่าที่ไม่ซ้ำกันของJobNameแต่เรารู้หลังจากอ่านแถวแรกถ้าเราต้องการ 1,000 แถวก่อนหน้า เราแทบไม่ต้องการแถวเหล่านั้น แต่เรายังต้องสแกนแถวต่อไป หากเรารู้ว่าแถวไม่หนาแน่นมากในตารางและเกือบทั้งหมดจะถูกกำจัดโดยการเข้าร่วมเราสามารถจินตนาการถึงรูปแบบ IO ที่มีประสิทธิภาพมากขึ้นในดัชนี เกิดอะไรขึ้นถ้า SQL Server อ่านแถวแรกตามค่าที่ไม่ซ้ำกันของJobNameตรวจสอบว่าค่านั้นอยู่ในDsAvgและเพียงข้ามไปข้างหน้าไปยังค่าถัดไปของJobNameถ้ามันเป็นอย่างไร แทนที่จะสแกน 212 ล้านแถวจะสามารถค้นหาแผนที่ต้องการการประหารชีวิตได้ประมาณ 200,000 ครั้งแทน

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

WITH RecursiveCTE
AS
(
    -- Anchor
    SELECT TOP (1)
        [JobName]
    FROM dbo.DsJobStat AS T
    ORDER BY
        T.[JobName]

    UNION ALL

    -- Recursive
    SELECT R.[JobName]
    FROM
    (
        -- Number the rows
        SELECT 
            T.[JobName],
            rn = ROW_NUMBER() OVER (
                ORDER BY T.[JobName])
        FROM dbo.DsJobStat AS T
        JOIN RecursiveCTE AS R
            ON R.[JobName] < T.[JobName]
    ) AS R
    WHERE
        -- Only the row that sorts lowest
        R.rn = 1
)
SELECT js.*
FROM RecursiveCTE
INNER JOIN dbo.DsJobStat js ON RecursiveCTE.[JobName]= js.[JobName]
WHERE NOT EXISTS (SELECT 1 FROM [DsAvg] WHERE RecursiveCTE.JobName = [DsAvg].JobName)
OPTION (MAXRECURSION 0);

แบบสอบถามที่เป็นจำนวนมากที่จะมองดังนั้นผมจึงขอแนะนำให้ตรวจสอบอย่างรอบคอบเป็นแผนการที่เกิดขึ้นจริง ก่อนอื่นเราทำ 200002 ดัชนีค้นหาเทียบกับดัชนีDsJobStatเพื่อรับJobNameค่าพิเศษทั้งหมด จากนั้นเราจะเข้าร่วมDsAvgและกำจัดแถวทั้งหมดยกเว้นแถวเดียว สำหรับแถวที่เหลือเข้าร่วมกลับไปDsJobStatและรับคอลัมน์ที่จำเป็นทั้งหมด

รูปแบบ IO เปลี่ยนแปลงโดยสิ้นเชิง ก่อนที่เราจะได้สิ่งนี้:

ตาราง 'DsJobStat' จำนวนการสแกน 1, ลอจิกอ่าน 1091651, การอ่านทางกายภาพ 13836, การอ่านล่วงหน้าอ่าน 181966

ด้วยแบบสอบถามแบบเรียกซ้ำเราได้รับสิ่งนี้:

ตาราง 'DsJobStat' จำนวนการสแกน 200003, การอ่านเชิงตรรกะ 1398000, การอ่านทางกายภาพ 1, การอ่านล่วงหน้าอ่าน 7345

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


NOT EXISTSผมแนะนำ พวกเขาตอบกลับด้วย "ฉันได้ลองทั้งเข้าร่วมและไม่มีอยู่ก่อนโพสต์คำถามไม่แตกต่างกันมาก"
Evan Carroll

1
ฉันอยากรู้อยากเห็นหากความคิดซ้ำ ๆ นั้นได้ผล
Evan Carroll

ฉันคิดว่าการมีประโยคไม่จำเป็นต้องใช้ "ElapsedSec ไม่ใช่โมฆะ" ในกรณีที่ประโยคจะทำยังฉันคิดว่า CTE แบบเรียกซ้ำไม่จำเป็นต้องใช้คุณสามารถใช้ row_number () เหนือ (พาร์ติชันตามลำดับชื่องาน) โดยที่ไม่มี คำถาม) คุณต้องพูดอะไรเกี่ยวกับความคิดของฉัน?
KumarHarsh

@Joe Obbish ฉันอัปเดตโพสต์ของฉัน ขอบคุณมาก.
เวนดี้

ใช่ CTE แบบเรียกซ้ำดำเนินการ row_number () บน (พาร์ติชันตามคำสั่งงานตามชื่อ) rn 1 นาที แต่ในเวลาเดียวกันฉันไม่เห็นกำไรพิเศษใด ๆ ใน Recursive CTE โดยใช้ข้อมูลตัวอย่างของคุณ
KumarHarsh

0

ดูว่าเกิดอะไรขึ้นถ้าคุณเขียนเงื่อนไขใหม่

AND DsJobStat.JobName NOT IN( SELECT [DsAvg].JobName FROM [DsAvg] )         

ถึง

AND NOT EXISTS ( SELECT 1 FROM [DsAvg] AS d WHERE d.JobName = DsJobStat.JobName )

ลองพิจารณาการเขียน SQL89 ของคุณใหม่เพราะสไตล์นั้นน่ากลัว

แทน

FROM DsJobStat, AJF 
WHERE DsJobStat.NumericOrderNo=AJF.OrderNo 
AND DsJobStat.Odate=AJF.Odate 

ลอง

FROM DsJobStat
INNER JOIN AJF ON (
  DsJobStat.NumericOrderNo=AJF.OrderNo 
  AND DsJobStat.Odate=AJF.Odate
)

ฉันยังสงสัยด้วยว่าเงื่อนไขนี้สามารถเขียนได้ดีกว่า แต่เราต้องรู้เพิ่มเติมเกี่ยวกับสิ่งที่เกิดขึ้น

HAVING AVG(CAST(DsJobStat.ElapsedSec AS FLOAT)) <> 0;

คุณต้องรู้ว่าค่าเฉลี่ยไม่เป็นศูนย์หรือแค่องค์ประกอบหนึ่งของกลุ่มนั้นไม่ได้เป็นศูนย์?


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