ข้อผิดพลาด:“ คำสั่ง INSERT EXEC ไม่สามารถซ้อนกันได้” และ“ ไม่สามารถใช้คำสั่ง ROLLBACK ภายในคำสั่ง INSERT-EXEC” วิธีแก้ปัญหานี้?


100

ฉันมีสามวิธีการจัดเก็บSp1, และSp2Sp3

คนแรก ( Sp1) จะดำเนินการอย่างใดอย่างหนึ่งที่สอง ( Sp2) และบันทึกข้อมูลที่ส่งกลับเข้ามา@tempTB1และคนที่สองจะดำเนินการหนึ่งในสาม ( Sp3) @tempTB2และบันทึกข้อมูลลงใน

หากฉันดำเนินการSp2มันจะใช้งานได้และจะส่งคืนข้อมูลทั้งหมดของฉันจากข้อมูลSp3แต่ปัญหาอยู่ที่Sp1เมื่อฉันดำเนินการมันจะแสดงข้อผิดพลาดนี้:

ไม่สามารถซ้อนคำสั่ง INSERT EXEC ได้

ฉันพยายามเปลี่ยนสถานที่execute Sp2และมันแสดงข้อผิดพลาดอื่นให้ฉัน:

ไม่สามารถใช้คำสั่ง ROLLBACK ภายในคำสั่ง INSERT-EXEC

คำตอบ:


102

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

ตัวอย่างเช่นวิธีแก้ปัญหาคือการเปลี่ยน Sp3 ให้เป็นฟังก์ชันที่มีมูลค่าตาราง


1
ลิงก์เสียหรือไซต์ที่ไม่ตอบสนอง
SouravA

6
คุณมีความคิดหรือไม่ว่าเหตุผลทางเทคนิคคืออะไรที่ไม่อนุญาต ฉันไม่พบข้อมูลใด ๆ เกี่ยวกับเรื่องนี้
jtate

1
น่าเสียดายที่นี่มักไม่ใช่ตัวเลือก ข้อมูลสำคัญหลายประเภทสามารถใช้ได้อย่างน่าเชื่อถือจากกระบวนงานที่จัดเก็บของระบบ(เนื่องจากในบางกรณีมุมมองการจัดการที่เกี่ยวข้องมีข้อมูลที่ไม่น่าเชื่อถือ / ล้าสมัยตัวอย่างคือข้อมูลที่ส่งคืนโดยsp_help_jobactivity)
GSerg

21

นี่เป็นวิธีเดียวที่ "ง่าย" ในการดำเนินการนี้ใน SQL Server โดยไม่มีฟังก์ชันที่สร้างขึ้นอย่างซับซ้อนขนาดใหญ่หรือเรียกใช้การเรียกสตริง sql ซึ่งทั้งสองวิธีนี้เป็นวิธีแก้ปัญหาที่แย่มาก:

  1. สร้างตารางชั่วคราว
  2. เปิดข้อมูลกระบวนงานที่เก็บไว้ของคุณลงไป

ตัวอย่าง:

INSERT INTO #YOUR_TEMP_TABLE
SELECT * FROM OPENROWSET ('SQLOLEDB','Server=(local);TRUSTED_CONNECTION=YES;','set fmtonly off EXEC [ServerName].dbo.[StoredProcedureName] 1,2,3')

หมายเหตุ : คุณต้องใช้ 'set fmtonly off' และคุณไม่สามารถเพิ่ม dynamic sql ให้กับสิ่งนี้ภายในการเรียก openrowset ทั้งสำหรับสตริงที่มีพารามิเตอร์โพรซีเดอร์ที่เก็บไว้ของคุณหรือสำหรับชื่อตาราง นั่นเป็นเหตุผลที่คุณต้องใช้ตารางชั่วคราวแทนตัวแปรตารางซึ่งน่าจะดีกว่าเนื่องจากมันมีผลกับตารางชั่วคราวในกรณีส่วนใหญ่


ไม่จำเป็นต้องใช้ SET FMTONLY OFF คุณสามารถเพิ่ม IF (1 = 0) ซึ่งส่งคืนตารางว่างที่มีชนิดข้อมูลเดียวกันกับที่โพรซีเดอร์ส่งคืนตามปกติ
Guillermo Gutiérrez

2
ตารางชั่วคราวและตัวแปรตารางจัดเก็บข้อมูลแตกต่างกันตัวแปรตารางควรใช้สำหรับชุดผลลัพธ์ขนาดเล็กเนื่องจากเครื่องมือเพิ่มประสิทธิภาพการสืบค้นไม่รักษาสถิติของตัวแปรตาราง ดังนั้นสำหรับชุดข้อมูลขนาดใหญ่มักจะดีกว่าที่จะใช้ตารางชั่วคราว นี่คือบทความดีๆในบล็อกที่mssqltips.com/sqlservertip/2825/…
gh9

@ gh9 ใช่ แต่นี่เป็นความคิดที่น่ากลัวสำหรับชุดผลลัพธ์ขนาดใหญ่อยู่ดี สถิติและการใช้ตารางจริงในฐานข้อมูลชั่วคราวอาจทำให้เกิดค่าใช้จ่ายที่สำคัญได้ ฉันมีขั้นตอนที่ส่งคืนชุดระเบียนที่มีค่าปัจจุบัน 1 แถว (การสอบถามหลายตาราง) และขั้นตอนที่เก็บข้อมูลนั้นในตัวแปรตารางและเปรียบเทียบกับค่าในตารางอื่นที่มีรูปแบบเดียวกัน การเปลี่ยนจากตารางชั่วคราวเป็นตัวแปรตารางทำให้เวลาเฉลี่ยเพิ่มขึ้นจาก 8 มิลลิวินาทีเป็น 2 มิลลิวินาทีซึ่งเป็นสิ่งสำคัญเมื่อมีการเรียกหลายครั้งต่อวินาทีตลอดทั้งวันและ 100,000 ครั้งในกระบวนการทุกคืน
Jason Goemaat

1
เหตุใดคุณจึงต้องการให้สถิติคงอยู่ในตัวแปรตาราง จุดรวมคือการสร้างตารางชั่วคราวใน RAM ซึ่งจะถูกทำลายหลังจากการสืบค้นเสร็จสิ้น ตามความหมายสถิติใด ๆ ที่สร้างบนตารางดังกล่าวจะไม่ถูกนำมาใช้ โดยทั่วไปความจริงที่ว่าข้อมูลในตัวแปรตารางจะยังคงอยู่ใน RAM ทุกที่ที่เป็นไปได้ทำให้เร็วกว่า Temp Tables ในทุกสถานการณ์ที่ข้อมูลของคุณมีขนาดเล็กกว่าจำนวน RAM ที่มีอยู่ใน SQL Server (ซึ่งในปัจจุบันนี้จะมีพูลหน่วยความจำ 100GB + สำหรับ SQL ของเรา เซิร์ฟเวอร์เกือบตลอดเวลา)
Geoff Griswald

วิธีนี้ใช้ไม่ได้กับโพรซีเดอร์ที่จัดเก็บเพิ่มเติม ข้อผิดพลาดคือไม่สามารถระบุข้อมูลเมตาได้เนื่องจากคำสั่ง 'ดำเนินการ <procedurename> @retval OUTPUT' ในขั้นตอน ... 'เรียกใช้กระบวนงานที่จัดเก็บเพิ่มเติม
GSerg

12

ตกลงได้รับการสนับสนุนจาก jimhark นี่คือตัวอย่างของแนวทางตารางแฮชเดี่ยวแบบเก่า: -

CREATE PROCEDURE SP3 as

BEGIN

    SELECT 1, 'Data1'
    UNION ALL
    SELECT 2, 'Data2'

END
go


CREATE PROCEDURE SP2 as

BEGIN

    if exists (select  * from tempdb.dbo.sysobjects o where o.xtype in ('U') and o.id = object_id(N'tempdb..#tmp1'))
        INSERT INTO #tmp1
        EXEC SP3
    else
        EXEC SP3

END
go

CREATE PROCEDURE SP1 as

BEGIN

    EXEC SP2

END
GO


/*
--I want some data back from SP3

-- Just run the SP1

EXEC SP1
*/


/*
--I want some data back from SP3 into a table to do something useful
--Try run this - get an error - can't nest Execs

if exists (select  * from tempdb.dbo.sysobjects o where o.xtype in ('U') and o.id = object_id(N'tempdb..#tmp1'))
    DROP TABLE #tmp1

CREATE TABLE #tmp1 (ID INT, Data VARCHAR(20))

INSERT INTO #tmp1
EXEC SP1


*/

/*
--I want some data back from SP3 into a table to do something useful
--However, if we run this single hash temp table it is in scope anyway so
--no need for the exec insert

if exists (select  * from tempdb.dbo.sysobjects o where o.xtype in ('U') and o.id = object_id(N'tempdb..#tmp1'))
    DROP TABLE #tmp1

CREATE TABLE #tmp1 (ID INT, Data VARCHAR(20))

EXEC SP1

SELECT * FROM #tmp1

*/

ฉันใช้วิธีแก้ปัญหานี้เช่นกัน ขอบคุณสำหรับความคิด!
SQL_Guy

การแก้ปัญหาที่ยอดเยี่ยม สิ่งนี้ช่วยให้ฉันเรียนรู้เพิ่มเติมเกี่ยวกับการกำหนดขอบเขตตารางอุณหภูมิ เช่นฉันไม่รู้ว่าคุณสามารถใช้ temp table ในสตริง dynsql ได้หากมีการประกาศภายนอก แนวคิดที่คล้ายกันที่นี่ ขอบคุณมาก.
jbd

10

วิธีแก้ปัญหาของฉันสำหรับปัญหานี้คือการใช้หลักการที่ว่าตารางชั่วคราวแฮชเดียวอยู่ในขอบเขตของ procs ที่เรียกว่า ดังนั้นฉันจึงมีสวิตช์ตัวเลือกในพารามิเตอร์ proc (ตั้งค่าเริ่มต้นเป็นปิด) หากเปิดใช้งาน proc ที่เรียกจะแทรกผลลัพธ์ลงในตารางชั่วคราวที่สร้างขึ้นในการโทร proc ฉันคิดว่าในอดีตฉันได้ดำเนินการไปอีกขั้นแล้วและใส่รหัสบางส่วนใน proc ที่เรียกว่าเพื่อตรวจสอบว่าตารางแฮชเดียวมีอยู่ในขอบเขตหรือไม่หากใส่รหัสแล้วจะส่งคืนชุดผลลัพธ์ ดูเหมือนจะทำงานได้ดี - วิธีที่ดีที่สุดในการส่งผ่านชุดข้อมูลขนาดใหญ่ระหว่าง procs


1
ฉันชอบคำตอบนี้และฉันพนันได้เลยว่าคุณจะได้รับคะแนนโหวตมากขึ้นหากคุณให้และยกตัวอย่าง
jimhark

ฉันทำแบบนี้มาหลายปีแล้ว ยังจำเป็นใน SQL Azure หรือไม่?
Nick Allan

ใช่ Azure ใช้เอ็นจิ้นเดียวกับ SQL Server ความแตกต่างที่แท้จริงเพียงอย่างเดียวคือที่เก็บข้อมูลและ CPU
Geoff Griswald

6

เคล็ดลับนี้ใช้ได้ผลกับฉัน

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

ทำกำไรให้กับสถานการณ์นั้นสำหรับวิธีแก้ปัญหาชั่วคราว

หากคุณมีสิทธิ์ที่ถูกต้องในการสร้างเซิร์ฟเวอร์ที่เชื่อมโยงให้ทำ สร้างเซิร์ฟเวอร์เดียวกันกับเซิร์ฟเวอร์ที่เชื่อมโยง

  • ใน SSMS เข้าสู่เซิร์ฟเวอร์ของคุณ
  • ไปที่ "Server Object
  • คลิกขวาที่ "เซิร์ฟเวอร์ที่เชื่อมโยง" จากนั้นคลิก "เซิร์ฟเวอร์ที่เชื่อมโยงใหม่"
  • ในกล่องโต้ตอบให้ตั้งชื่อเซิร์ฟเวอร์ที่เชื่อมโยงของคุณเช่น: THISSERVER
  • ประเภทเซิร์ฟเวอร์คือ "แหล่งข้อมูลอื่น"
  • ผู้ให้บริการ: Microsoft OLE DB Provider สำหรับ SQL server
  • แหล่งข้อมูล: IP ของคุณอาจเป็นเพียงจุด (.) เนื่องจากเป็น localhost
  • ไปที่แท็บ "ความปลอดภัย" และเลือกอันที่ 3 "สร้างขึ้นโดยใช้บริบทความปลอดภัยปัจจุบันของการเข้าสู่ระบบ"
  • คุณสามารถแก้ไขตัวเลือกเซิร์ฟเวอร์ (แท็บที่ 3) ได้หากต้องการ
  • กดตกลงเซิร์ฟเวอร์ที่เชื่อมโยงของคุณถูกสร้างขึ้น

ตอนนี้คำสั่ง Sql ของคุณใน SP1 คือ

insert into @myTempTable
exec THISSERVER.MY_DATABASE_NAME.MY_SCHEMA.SP2

เชื่อฉันเถอะว่ามันใช้งานได้แม้คุณจะมีไดนามิกแทรกใน SP2


4

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


แต่ข้อเสียคือปัญหาในการจัดการข้อยกเว้นถ้าฟังก์ชันซับซ้อนใช่ไหม?
Muflix

3

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

คำตอบ OPENROWSET ที่ยอมรับใช้งานได้ดี แต่ฉันต้องการหลีกเลี่ยงการใช้ Dynamic SQL หรือผู้ให้บริการ OLE ภายนอกในกระบวนการของฉันดังนั้นฉันจึงไปเส้นทางอื่น

วิธีแก้ปัญหาง่ายๆอย่างหนึ่งที่ฉันพบคือเปลี่ยนตารางชั่วคราวในกระบวนงานที่เก็บไว้ของฉันเป็นตัวแปรตาราง มันทำงานเหมือนกับที่ทำกับตารางชั่วคราว แต่ไม่ขัดแย้งกับการแทรกตารางชั่วคราวอื่น ๆ ของฉันอีกต่อไป

เพื่อหลีกเลี่ยงความคิดเห็นที่ฉันรู้ว่าพวกคุณสองสามคนกำลังจะเขียนเตือนฉันว่า Table Variables เป็นตัวฆ่าประสิทธิภาพ ... ทั้งหมดที่ฉันสามารถพูดกับคุณก็คือในปี 2020 จะจ่ายเงินปันผลโดยไม่ต้องกลัว Table Variables ถ้านี่เป็นปี 2008 และฐานข้อมูลของฉันโฮสต์บนเซิร์ฟเวอร์ที่มี RAM 16GB และใช้งาน HDD 5400RPM ฉันอาจเห็นด้วยกับคุณ แต่มันเป็นปี 2020 และฉันมีอาร์เรย์ SSD เป็นที่เก็บข้อมูลหลักและแรมหลายร้อยกิ๊ก ฉันสามารถโหลดฐานข้อมูลทั้ง บริษัท ของฉันไปยังตัวแปรตารางและยังมี RAM เหลืออยู่มากมาย

Table Variables กลับมาอยู่ในเมนูแล้ว!


1

ฉันมีปัญหาเดียวกันและกังวลเกี่ยวกับรหัสที่ซ้ำกันในสอง sprocs ขึ้นไป ฉันลงเอยด้วยการเพิ่มแอตทริบิวต์เพิ่มเติมสำหรับ "โหมด" รหัสทั่วไปที่อนุญาตให้มีอยู่ภายในหนึ่ง sproc และโหมดกำกับโฟลว์และชุดผลลัพธ์ของ sproc


1

สิ่งที่เกี่ยวกับการจัดเก็บผลลัพธ์ไว้ในตารางคงที่? ชอบ

-- SubProcedure: subProcedureName
---------------------------------
-- Save the value
DELETE lastValue_subProcedureName
INSERT INTO lastValue_subProcedureName (Value)
SELECT @Value
-- Return the value
SELECT @Value

-- Procedure
--------------------------------------------
-- get last value of subProcedureName
SELECT Value FROM lastValue_subProcedureName

มันไม่เหมาะ แต่มันง่ายมากและคุณไม่จำเป็นต้องเขียนใหม่ทุกอย่าง

อัปเดต : โซลูชันก่อนหน้านี้ทำงานได้ไม่ดีกับแบบสอบถามแบบขนาน (การเข้าถึงแบบ async และผู้ใช้หลายคน) ดังนั้นตอนนี้ฉันใช้ตารางชั่วคราว

-- A local temporary table created in a stored procedure is dropped automatically when the stored procedure is finished. 
-- The table can be referenced by any nested stored procedures executed by the stored procedure that created the table. 
-- The table cannot be referenced by the process that called the stored procedure that created the table.
IF OBJECT_ID('tempdb..#lastValue_spGetData') IS NULL
CREATE TABLE #lastValue_spGetData (Value INT)

-- trigger stored procedure with special silent parameter
EXEC dbo.spGetData 1 --silent mode parameter

spGetDataเนื้อหากระบวนงานที่เก็บไว้ที่ซ้อนกัน

-- Save the output if temporary table exists.
IF OBJECT_ID('tempdb..#lastValue_spGetData') IS NOT NULL
BEGIN
    DELETE #lastValue_spGetData
    INSERT INTO #lastValue_spGetData(Value)
    SELECT Col1 FROM dbo.Table1
END

 -- stored procedure return
 IF @silentMode = 0
 SELECT Col1 FROM dbo.Table1

โดยทั่วไปคุณไม่สามารถสร้าง SProc ad-hoc ได้เหมือนที่คุณทำได้ด้วย Tables คุณจะต้องขยายตัวอย่างของคุณด้วยการอ้างอิงเพิ่มเติมเนื่องจากแนวทางนี้ยังไม่เป็นที่รู้จักหรือยอมรับได้จริงๆ นอกจากนี้ยังมีลักษณะคล้าย Lambda Expression มากกว่าการดำเนินการ SProc ซึ่ง ANSI-SQL ไม่อนุญาตให้ใช้วิธี Lambda Expression
GoldBishop

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

1
ตรรกะของตาราง Temp นั้นดีมันเป็นการอ้างอิง SProc ที่ฉันกังวล Sproc ไม่สามารถสอบถามได้โดยตรง ฟังก์ชัน Table-Valued สามารถสอบถามได้โดยตรงจาก ต้องชอบที่คุณพูดถึงในตรรกะที่อัปเดตแนวทางที่ดีที่สุดคือ Temp Table เซสชันอินสแตนซ์หรือส่วนกลางและดำเนินการจากจุดนั้น
GoldBishop

0

ประกาศตัวแปรเคอร์เซอร์เอาต์พุตเป็น sp ด้านใน:

@c CURSOR VARYING OUTPUT

จากนั้นประกาศเคอร์เซอร์ c ไปยังรายการที่คุณต้องการกลับ จากนั้นเปิดเคอร์เซอร์ จากนั้นตั้งค่าการอ้างอิง:

DECLARE c CURSOR LOCAL FAST_FORWARD READ_ONLY FOR 
SELECT ...
OPEN c
SET @c = c 

อย่าปิดหรือจัดสรรใหม่

ตอนนี้เรียก sp ภายในจากด้านนอกที่จัดหาพารามิเตอร์เคอร์เซอร์เช่น:

exec sp_abc a,b,c,, @cOUT OUTPUT

เมื่อ sp ภายในดำเนินการคุณ @cOUTก็พร้อมที่จะดึงข้อมูล วนซ้ำแล้วปิดและยกเลิกการจัดสรร


0

หากคุณสามารถใช้เทคโนโลยีที่เกี่ยวข้องอื่น ๆ เช่น C # ฉันขอแนะนำให้ใช้คำสั่ง SQL ในตัวที่มีพารามิเตอร์ Transaction

var sqlCommand = new SqlCommand(commandText, null, transaction);

ฉันได้สร้างแอปคอนโซลง่ายๆที่แสดงความสามารถนี้ซึ่งสามารถพบได้ที่นี่: https://github.com/hecked12/SQL-Transaction-Using-C-Sharp

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


-1

บน SQL Server 2008 R2 ฉันมีคอลัมน์ในตารางที่ไม่ตรงกันซึ่งทำให้เกิดข้อผิดพลาดในการย้อนกลับ มันหายไปเมื่อฉันแก้ไขตัวแปรตาราง sqlcmd ที่เติมโดยคำสั่ง insert-exec เพื่อให้ตรงกับที่ส่งคืนโดย proc ที่เก็บไว้ ไม่มี org_code ในไฟล์ windows cmd จะโหลดผลลัพธ์ของขั้นตอนการจัดเก็บและเลือก

set SQLTXT= declare @resets as table (org_id nvarchar(9), org_code char(4), ^
tin(char9), old_strt_dt char(10), strt_dt char(10)); ^
insert @resets exec rsp_reset; ^
select * from @resets;

sqlcmd -U user -P pass -d database -S server -Q "%SQLTXT%" -o "OrgReport.txt"

OP กำลังถามเกี่ยวกับข้อผิดพลาดที่เกิดขึ้นเมื่อใช้คำสั่ง insert-exec ในโพรซีเดอร์ที่เก็บแบบซ้อนกัน ปัญหาของคุณจะส่งคืนข้อผิดพลาดที่แตกต่างกันเช่น "รายการที่เลือกสำหรับคำสั่ง INSERT มีรายการน้อยกว่ารายการแทรกจำนวนค่า SELECT ต้องตรงกับจำนวนคอลัมน์ INSERT"
Losbear

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