ก่อนอื่นฉันขอโทษสำหรับความล่าช้าในการตอบสนองของฉันตั้งแต่ความคิดเห็นล่าสุดของฉัน
หัวเรื่องปรากฏขึ้นในความคิดเห็นที่ใช้ Recursive CTE (rCTE จากที่นี่เป็นต้นไป) วิ่งเร็วพอเนื่องจากมีจำนวนแถวน้อย แม้ว่าอาจดูเหมือนเป็นอย่างนั้น แต่ก็ไม่มีอะไรเพิ่มเติมจากความจริง
สร้างตารางที่สูงและฟังก์ชั่นที่ยอดเยี่ยม
ก่อนที่เราจะเริ่มการทดสอบเราจำเป็นต้องสร้างตาราง Tally แบบกายภาพด้วยดัชนี Clustered ที่เหมาะสมและฟังก์ชัน Tally ของสไตล์ Itzik Ben-Gan เราจะทำสิ่งนี้ทั้งหมดใน TempDB เพื่อที่เราจะได้ไม่ทิ้งสารพัดของทุกคนโดยไม่ตั้งใจ
นี่คือรหัสในการสร้าง Tally Table และเวอร์ชันการผลิตปัจจุบันของฉันในรหัสที่ยอดเยี่ยมของ Itzik
--===== Do this in a nice, safe place that everyone has
USE tempdb
;
--===== Create/Recreate a Physical Tally Table
IF OBJECT_ID('dbo.Tally','U') IS NOT NULL
DROP TABLE dbo.Tally
;
-- Note that the ISNULL makes a NOT NULL column
SELECT TOP 1000001
N = ISNULL(ROW_NUMBER() OVER (ORDER BY (SELECT NULL))-1,0)
INTO dbo.Tally
FROM sys.all_columns ac1
CROSS JOIN sys.all_columns ac2
;
ALTER TABLE dbo.Tally
ADD CONSTRAINT PK_Tally PRIMARY KEY CLUSTERED (N)
;
--===== Create/Recreate a Tally Function
IF OBJECT_ID('dbo.fnTally','IF') IS NOT NULL
DROP FUNCTION dbo.fnTally
;
GO
CREATE FUNCTION [dbo].[fnTally]
/**********************************************************************************************************************
Purpose:
Return a column of BIGINTs from @ZeroOrOne up to and including @MaxN with a max value of 1 Trillion.
As a performance note, it takes about 00:02:10 (hh:mm:ss) to generate 1 Billion numbers to a throw-away variable.
Usage:
--===== Syntax example (Returns BIGINT)
SELECT t.N
FROM dbo.fnTally(@ZeroOrOne,@MaxN) t
;
Notes:
1. Based on Itzik Ben-Gan's cascading CTE (cCTE) method for creating a "readless" Tally Table source of BIGINTs.
Refer to the following URLs for how it works and introduction for how it replaces certain loops.
http://www.sqlservercentral.com/articles/T-SQL/62867/
http://sqlmag.com/sql-server/virtual-auxiliary-table-numbers
2. To start a sequence at 0, @ZeroOrOne must be 0 or NULL. Any other value that's convertable to the BIT data-type
will cause the sequence to start at 1.
3. If @ZeroOrOne = 1 and @MaxN = 0, no rows will be returned.
5. If @MaxN is negative or NULL, a "TOP" error will be returned.
6. @MaxN must be a positive number from >= the value of @ZeroOrOne up to and including 1 Billion. If a larger
number is used, the function will silently truncate after 1 Billion. If you actually need a sequence with
that many values, you should consider using a different tool. ;-)
7. There will be a substantial reduction in performance if "N" is sorted in descending order. If a descending
sort is required, use code similar to the following. Performance will decrease by about 27% but it's still
very fast especially compared with just doing a simple descending sort on "N", which is about 20 times slower.
If @ZeroOrOne is a 0, in this case, remove the "+1" from the code.
DECLARE @MaxN BIGINT;
SELECT @MaxN = 1000;
SELECT DescendingN = @MaxN-N+1
FROM dbo.fnTally(1,@MaxN);
8. There is no performance penalty for sorting "N" in ascending order because the output is explicity sorted by
ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
Revision History:
Rev 00 - Unknown - Jeff Moden
- Initial creation with error handling for @MaxN.
Rev 01 - 09 Feb 2013 - Jeff Moden
- Modified to start at 0 or 1.
Rev 02 - 16 May 2013 - Jeff Moden
- Removed error handling for @MaxN because of exceptional cases.
Rev 03 - 22 Apr 2015 - Jeff Moden
- Modify to handle 1 Trillion rows for experimental purposes.
**********************************************************************************************************************/
(@ZeroOrOne BIT, @MaxN BIGINT)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN WITH
E1(N) AS (SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1) --10E1 or 10 rows
, E4(N) AS (SELECT 1 FROM E1 a, E1 b, E1 c, E1 d) --10E4 or 10 Thousand rows
,E12(N) AS (SELECT 1 FROM E4 a, E4 b, E4 c) --10E12 or 1 Trillion rows
SELECT N = 0 WHERE ISNULL(@ZeroOrOne,0)= 0 --Conditionally start at 0.
UNION ALL
SELECT TOP(@MaxN) N = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E12 -- Values from 1 to @MaxN
;
GO
โดยวิธีการ ... สังเกตเห็นว่าสร้างตาราง Tally Table หนึ่งล้านและหนึ่งแถวและเพิ่มดัชนีแบบกลุ่มในดัชนีประมาณหนึ่งวินาที ลองใช้ด้วย rCTE และดูว่าใช้เวลานานเท่าใด! ;-)
สร้างข้อมูลการทดสอบบางส่วน
เราต้องการข้อมูลการทดสอบด้วย ใช่ฉันยอมรับว่าฟังก์ชั่นทั้งหมดที่เรากำลังจะทำการทดสอบรวมถึง rCTE ทำงานเป็นมิลลิวินาทีหรือน้อยกว่าในเวลาเพียง 12 แถว แต่นั่นเป็นกับดักที่ผู้คนจำนวนมากตกอยู่ใน เราจะพูดเพิ่มเติมเกี่ยวกับกับดักในภายหลัง แต่สำหรับตอนนี้ลองจำลองการโทรแต่ละฟังก์ชั่น 40,000 ครั้งซึ่งประมาณว่าฟังก์ชั่นบางอย่างในร้านของฉันถูกเรียกในเวลา 8 ชั่วโมงต่อวัน แค่คิดว่าอาจมีการเรียกใช้ฟังก์ชั่นนี้กี่ครั้งในธุรกิจค้าปลีกออนไลน์ขนาดใหญ่
ดังนั้นนี่คือรหัสในการสร้าง 40,000 แถวด้วยวันที่สุ่มโดยแต่ละครั้งจะมีหมายเลขแถวเพื่อการติดตาม ฉันไม่ได้ใช้เวลาเพื่อให้เวลาทั้งชั่วโมงเพราะมันไม่สำคัญ
--===== Do this in a nice, safe place that everyone has
USE tempdb
;
--===== Create/Recreate a Test Date table
IF OBJECT_ID('dbo.TestDate','U') IS NOT NULL
DROP TABLE dbo.TestDate
;
DECLARE @StartDate DATETIME
,@EndDate DATETIME
,@Rows INT
;
SELECT @StartDate = '2010' --Inclusive
,@EndDate = '2020' --Exclusive
,@Rows = 40000 --Enough to simulate an 8 hour day where I work
;
SELECT RowNum = IDENTITY(INT,1,1)
,SomeDateTime = RAND(CHECKSUM(NEWID()))*DATEDIFF(dd,@StartDate,@EndDate)+@StartDate
INTO dbo.TestDate
FROM dbo.fnTally(1,@Rows)
;
สร้างฟังก์ชั่นบางอย่างเพื่อทำสิ่ง 12 แถวต่อชั่วโมง
ถัดไปฉันแปลงโค้ด rCTE เป็นฟังก์ชันและสร้างอีก 3 ฟังก์ชัน พวกเขาทั้งหมดถูกสร้างขึ้นเป็น iTVF ที่มีประสิทธิภาพสูง (ฟังก์ชั่นที่มีค่าในตารางแบบอินไลน์) คุณสามารถบอกได้เสมอเพราะ iTVF ไม่มี BEGIN ในตัวเช่น Scalar หรือ mTVFs (ฟังก์ชั่นประเมินมูลค่าตารางแบบหลายคำสั่ง)
นี่คือรหัสในการสร้างฟังก์ชั่นทั้งสี่นั้น ... ฉันตั้งชื่อพวกเขาหลังจากวิธีที่พวกเขาใช้และไม่ใช่สิ่งที่พวกเขาทำเพียงเพื่อให้ง่ายต่อการระบุพวกเขา
--===== CREATE THE iTVFs
--===== Do this in a nice, safe place that everyone has
USE tempdb
;
-----------------------------------------------------------------------------------------
IF OBJECT_ID('dbo.OriginalrCTE','IF') IS NOT NULL
DROP FUNCTION dbo.OriginalrCTE
;
GO
CREATE FUNCTION dbo.OriginalrCTE
(@Date DATETIME)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
WITH Dates AS
(
SELECT DATEPART(HOUR,DATEADD(HOUR,-1,@Date)) [Hour],
DATEADD(HOUR,-1,@Date) [Date], 1 Num
UNION ALL
SELECT DATEPART(HOUR,DATEADD(HOUR,-1,[Date])),
DATEADD(HOUR,-1,[Date]), Num+1
FROM Dates
WHERE Num <= 11
)
SELECT [Hour], [Date]
FROM Dates
GO
-----------------------------------------------------------------------------------------
IF OBJECT_ID('dbo.MicroTally','IF') IS NOT NULL
DROP FUNCTION dbo.MicroTally
;
GO
CREATE FUNCTION dbo.MicroTally
(@Date DATETIME)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
SELECT [Hour] = DATEPART(HOUR,DATEADD(HOUR,t.N,@Date))
,[DATE] = DATEADD(HOUR,t.N,@Date)
FROM (VALUES (-1),(-2),(-3),(-4),(-5),(-6),(-7),(-8),(-9),(-10),(-11),(-12))t(N)
;
GO
-----------------------------------------------------------------------------------------
IF OBJECT_ID('dbo.PhysicalTally','IF') IS NOT NULL
DROP FUNCTION dbo.PhysicalTally
;
GO
CREATE FUNCTION dbo.PhysicalTally
(@Date DATETIME)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
SELECT [Hour] = DATEPART(HOUR,DATEADD(HOUR,-t.N,@Date))
,[DATE] = DATEADD(HOUR,-t.N,@Date)
FROM dbo.Tally t
WHERE N BETWEEN 1 AND 12
;
GO
-----------------------------------------------------------------------------------------
IF OBJECT_ID('dbo.TallyFunction','IF') IS NOT NULL
DROP FUNCTION dbo.TallyFunction
;
GO
CREATE FUNCTION dbo.TallyFunction
(@Date DATETIME)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
SELECT [Hour] = DATEPART(HOUR,DATEADD(HOUR,-t.N,@Date))
,[DATE] = DATEADD(HOUR,-t.N,@Date)
FROM dbo.fnTally(1,12) t
;
GO
สร้างชุดทดสอบเพื่อทดสอบฟังก์ชั่น
สุดท้าย แต่ไม่ท้ายสุดเราต้องใช้สายรัดทดสอบ ฉันจะตรวจสอบพื้นฐานแล้วทดสอบแต่ละฟังก์ชั่นในลักษณะที่เหมือนกัน
นี่คือรหัสสำหรับชุดทดสอบ ...
PRINT '--========== Baseline Select =================================';
DECLARE @Hour INT, @Date DATETIME
;
SET STATISTICS TIME,IO ON;
SELECT @Hour = RowNum
,@Date = SomeDateTime
FROM dbo.TestDate
CROSS APPLY dbo.fnTally(1,12);
SET STATISTICS TIME,IO OFF;
GO
PRINT '--========== Orginal Recursive CTE ===========================';
DECLARE @Hour INT, @Date DATETIME
;
SET STATISTICS TIME,IO ON;
SELECT @Hour = fn.[Hour]
,@Date = fn.[Date]
FROM dbo.TestDate td
CROSS APPLY dbo.OriginalrCTE(td.SomeDateTime) fn;
SET STATISTICS TIME,IO OFF;
GO
PRINT '--========== Dedicated Micro-Tally Table =====================';
DECLARE @Hour INT, @Date DATETIME
;
SET STATISTICS TIME,IO ON;
SELECT @Hour = fn.[Hour]
,@Date = fn.[Date]
FROM dbo.TestDate td
CROSS APPLY dbo.MicroTally(td.SomeDateTime) fn;
SET STATISTICS TIME,IO OFF;
GO
PRINT'--========== Physical Tally Table =============================';
DECLARE @Hour INT, @Date DATETIME
;
SET STATISTICS TIME,IO ON;
SELECT @Hour = fn.[Hour]
,@Date = fn.[Date]
FROM dbo.TestDate td
CROSS APPLY dbo.PhysicalTally(td.SomeDateTime) fn;
SET STATISTICS TIME,IO OFF;
GO
PRINT'--========== Tally Function ===================================';
DECLARE @Hour INT, @Date DATETIME
;
SET STATISTICS TIME,IO ON;
SELECT @Hour = fn.[Hour]
,@Date = fn.[Date]
FROM dbo.TestDate td
CROSS APPLY dbo.TallyFunction(td.SomeDateTime) fn;
SET STATISTICS TIME,IO OFF;
GO
สิ่งหนึ่งที่สังเกตได้ในชุดทดสอบด้านบนคือฉันจะแบ่งเอาต์พุตทั้งหมดเป็นตัวแปร "throwaway" นั่นคือเพื่อพยายามรักษาประสิทธิภาพการวัดให้บริสุทธิ์ที่สุดเท่าที่จะเป็นไปได้โดยไม่มีผลลัพธ์ใด ๆ ลงบนดิสก์หรือหน้าจอที่บิดเบือน
คำเตือนเกี่ยวกับสถิติการตั้งค่า
นอกจากนี้ยังมีคำเตือนสำหรับผู้ที่จะทดสอบ ... คุณจะต้องไม่ใช้ SET Statistics เมื่อทำการทดสอบฟังก์ชั่น Scalar หรือ mTVF สามารถใช้งานได้อย่างปลอดภัยกับฟังก์ชั่น iTVF เช่นเดียวกับที่ใช้ในการทดสอบนี้ สถิติของตลาดหลักทรัพย์ได้รับการพิสูจน์แล้วว่าทำให้ฟังก์ชัน SCALAR ทำงานช้าลงกว่าที่พวกเขาทำไว้หลายร้อยเท่า ใช่ฉันพยายามเอียงกังหันลมอีกเครื่อง แต่นั่นจะเป็นบทความที่มีความยาวทั้งหมดและฉันไม่มีเวลาทำเช่นนั้น ฉันมีบทความเกี่ยวกับ SQLServerCentral.com ที่พูดถึงเรื่องนี้ทั้งหมด แต่ไม่มีเหตุผลในการโพสต์ลิงค์ที่นี่เพราะบางคนจะงอออกจากรูปร่างเกี่ยวกับมัน
ผลการทดสอบ
ดังนั้นนี่คือผลการทดสอบเมื่อฉันรันชุดทดสอบบนแล็ปท็อป i5 ตัวเล็ก ๆ ที่มี RAM ขนาด 6GB
--========== Baseline Select =================================
Table 'Worktable'. Scan count 1, logical reads 82309, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TestDate'. Scan count 1, logical reads 105, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 203 ms, elapsed time = 206 ms.
--========== Orginal Recursive CTE ===========================
Table 'Worktable'. Scan count 40001, logical reads 2960000, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TestDate'. Scan count 1, logical reads 105, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 4258 ms, elapsed time = 4415 ms.
--========== Dedicated Micro-Tally Table =====================
Table 'Worktable'. Scan count 1, logical reads 81989, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TestDate'. Scan count 1, logical reads 105, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 234 ms, elapsed time = 235 ms.
--========== Physical Tally Table =============================
Table 'Worktable'. Scan count 1, logical reads 81989, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TestDate'. Scan count 1, logical reads 105, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Tally'. Scan count 1, logical reads 3, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 250 ms, elapsed time = 252 ms.
--========== Tally Function ===================================
Table 'Worktable'. Scan count 1, logical reads 81989, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TestDate'. Scan count 1, logical reads 105, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 250 ms, elapsed time = 253 ms.
"เลือกขั้นพื้นฐาน" ซึ่งเลือกเฉพาะข้อมูล (แต่ละแถวสร้าง 12 ครั้งเพื่อจำลองปริมาณการส่งคืนเดียวกัน) มาทางขวาประมาณ 1 / 5th ของวินาที ทุกอย่างเข้ามาในเวลาประมาณหนึ่งในสี่ของวินาที ทุกอย่างยกเว้นฟังก์ชั่น rCTE เลือด ใช้เวลา 4 และ 1/4 วินาทีหรือนานกว่า 16 เท่า (ช้ากว่า 1,600%)
และดูที่การอ่านแบบลอจิคัล (หน่วยความจำ IO) ... rCTE ใช้งาน 2,960,000 (เกือบ 3 ล้านอ่าน) ในขณะที่ฟังก์ชั่นอื่น ๆ ใช้เพียงประมาณ 82,100 นั่นหมายความว่า rCTE ใช้หน่วยความจำ IO มากกว่า 34.3 เท่าของฟังก์ชั่นอื่น ๆ
ความคิดที่ปิด
มาสรุปกัน วิธี rCTE สำหรับทำสิ่ง 12 แถว "เล็ก" นี้ใช้ซีพียู (และระยะเวลา) 16 ครั้ง (1,600%) และ 34.3 ครั้ง (3,430%) หน่วยความจำ IO มากกว่าฟังก์ชั่นอื่น ๆ
เฮ้ ... ฉันรู้ว่าคุณกำลังคิดอะไรอยู่ "เรื่องใหญ่! มันเป็นแค่ฟังก์ชั่นเดียว"
ใช่เห็นด้วย แต่คุณมีฟังก์ชั่นอื่นอีกกี่ตัว? คุณมีสถานที่อื่นอีกกี่แห่งนอกฟังก์ชั่น? และคุณมีผู้ที่ทำงานมากกว่า 12 แถวในแต่ละครั้งหรือไม่? และมีโอกาสที่ใครบางคนในเซถลาวิธีอาจคัดลอกรหัส rCTE สำหรับสิ่งที่ใหญ่กว่ามาก?
ตกลงเวลาที่จะทื่อ มันไม่สมเหตุสมผลเลยที่คนจะพิสูจน์ประสิทธิภาพของรหัสที่ท้าทายเนื่องจากจำนวนแถวหรือการใช้งานที่ จำกัด ยกเว้นเมื่อคุณซื้อกล่อง MPP สำหรับหลายล้านดอลลาร์ (ไม่ต้องพูดถึงค่าใช้จ่ายของการเขียนรหัสใหม่เพื่อให้มันทำงานบนเครื่องดังกล่าวได้) คุณไม่สามารถซื้อเครื่องที่รันรหัสของคุณได้เร็วขึ้น 16 เท่า (ชนะของ SSD อย่าทำเช่นนี้ ... ทุกสิ่งนี้อยู่ในหน่วยความจำความเร็วสูงเมื่อเราทำการทดสอบ) ประสิทธิภาพการทำงานอยู่ในรหัส ประสิทธิภาพที่ดีอยู่ในรหัสที่ดี
คุณจินตนาการได้ไหมว่ารหัสทั้งหมดของคุณทำงานเร็วขึ้น 16 เท่า?
อย่าปรับรหัสที่ไม่ถูกต้องหรือประสิทธิภาพที่ท้าทายให้กับจำนวนแถวต่ำหรือแม้แต่การใช้งานต่ำ หากคุณทำเช่นนั้นคุณอาจต้องยืมหนึ่งในกังหันลมที่ฉันถูกกล่าวหาว่าเอียงที่จะทำให้ซีพียูและดิสก์ของคุณเย็นพอ ;-)
WORD ON WORD "TALLY"
ใช่ ... ฉันเห็นด้วย การพูดความหมายตาราง Tally มีตัวเลขไม่ใช่ "การนับ" ในบทความต้นฉบับของฉันเกี่ยวกับหัวเรื่อง (ไม่ใช่บทความต้นฉบับเกี่ยวกับเทคนิค แต่เป็นบทความแรกของฉัน) ฉันเรียกมันว่า "Tally" ไม่ใช่เพราะสิ่งที่มีอยู่ แต่เพราะสิ่งที่มันทำ ... มันเป็น ใช้เพื่อ "นับ" แทนการวนซ้ำและบางสิ่งที่ "นับ" คือการนับบางสิ่ง ;-) เรียกมันว่าสิ่งที่คุณจะ ... ตารางตัวเลขโต๊ะ Tally, ตารางลำดับอะไรก็ตาม ฉันไม่สนใจ สำหรับฉัน "Tally" นั้นมีความหมายมากกว่าและเป็น DBA ขี้เกียจที่ดีมีเพียง 5 ตัวอักษร (2 เหมือนกัน) แทนที่จะเป็น 7 และง่ายกว่าที่จะพูดกับคนส่วนใหญ่ นอกจากนี้ยังเป็น "เอกพจน์" ซึ่งทำตามแบบแผนการตั้งชื่อของฉันสำหรับตาราง ;-) มัน ' s สิ่งที่บทความที่มีหน้าจากหนังสือจาก 60 ที่เรียกว่า ฉันมักจะอ้างถึงมันเป็น "Tally Table" และคุณจะยังคงรู้ว่าฉันหรือคนอื่นหมายถึงอะไร ฉันยังหลีกเลี่ยงสัญลักษณ์ฮังการีเหมือนกาฬโรค แต่เรียกใช้ฟังก์ชัน "fnTally" เพื่อที่ฉันจะได้พูดว่า "ดีถ้าคุณใช้ฟังก์ชั่น Tally-en Tally ที่ฉันแสดงให้คุณเห็นคุณจะไม่มีปัญหาเรื่องประสิทธิภาพ" โดยแท้จริงแล้ว การละเมิดทรัพยากรบุคคล ;-) จริงๆแล้วมันจะไม่เป็นการละเมิดทรัพยากรบุคคล ;-) จริงๆแล้วมันจะไม่เป็นการละเมิดทรัพยากรบุคคล ;-)
สิ่งที่ฉันกังวลมากกว่าคือคนที่เรียนรู้ที่จะใช้อย่างถูกต้องแทนที่จะหันไปใช้สิ่งต่าง ๆ เช่นประสิทธิภาพที่ท้าทาย rCTEs และ Hidden RBAR ในรูปแบบอื่น ๆ