Czy jest możliwe, aby klauzula SQL Output zwracała kolumnę, która nie została wstawiona?

124

Dokonałem pewnych modyfikacji w mojej bazie danych i muszę przenieść stare dane do nowych tabel. W tym celu muszę wypełnić tabelę (ReportOptions), biorąc dane z oryginalnej tabeli (Practice), i wypełnić drugą tabelę pośrednią (PracticeReportOption).

ReportOption (ReportOptionId int PK, field1, field2...)
Practice (PracticeId int PK, field1, field2...)
PracticeReportOption (PracticeReportOptionId int PK, PracticeId int FK, ReportOptionId int FK, field1, field2...)

Wykonałem zapytanie, aby uzyskać wszystkie dane potrzebne do przejścia z Practice do ReportOptions, ale mam problem z wypełnieniem tabeli pośredniej

--Auxiliary tables
DECLARE @ReportOption TABLE (PracticeId int /*This field is not on the actual ReportOption table*/, field1, field2...)
DECLARE @PracticeReportOption TABLE (PracticeId int, ReportOptionId int, field1, field2)

--First I get all the data I need to move
INSERT INTO @ReportOption
SELECT P.practiceId, field1, field2...
  FROM Practice P

--I insert it into the new table, but somehow I need to have the repation PracticeId / ReportOptionId
INSERT INTO ReportOption (field1, field2...)
OUTPUT @ReportOption.PracticeId, --> this is the field I don't know how to get
       inserted.ReportOptionId
  INTO @PracticeReportOption (PracticeId, ReportOptionId)
SELECT field1, field2
  FROM @ReportOption

--This would insert the relationship, If I knew how to get it!
INSERT INTO @PracticeReportOption (PracticeId, ReportOptionId)
SELECT PracticeId, ReportOptionId
  FROM @ReportOption

Gdybym mógł odwołać się do pola, którego nie ma w tabeli docelowej w klauzuli OUTPUT, byłoby świetnie (myślę, że nie mogę, ale nie wiem na pewno). Jakieś pomysły, jak zaspokoić moje potrzeby?

Alejandro B.
źródło
1
W OUTPUTklauzuli możesz zwrócić dowolną kolumnę tabeli, do której wstawiony jest wiersz . Więc nawet jeśli nie podasz wartości dla danej kolumny w swoim INSERTwyciągu, nadal możesz określić tę kolumnę w OUTPUTklauzuli. Nie można jednak zwracać zmiennych SQL ani kolumn z innych tabel.
marc_s
2
@marc_s dzięki za odpowiedź, ale nie mam pola, którego potrzebuję w tabeli docelowej (potrzebuję PracticeId, którego nie ma w ReportOption)
Alejandro

Odpowiedzi:

192

Możesz to zrobić, używając MERGEzamiast wstawiania:

więc wymień to

INSERT INTO ReportOption (field1, field2...)
OUTPUT @ReportOption.PracticeId, --> this is the field I don't know how to get
       inserted.ReportOptionId
  INTO @PracticeReportOption (PracticeId, ReportOptionId)
SELECT field1, field2
  FROM @ReportOption

z

MERGE INTO ReportOption USING @ReportOption AS temp ON 1 = 0
WHEN NOT MATCHED THEN
    INSERT (field1, field2)
    VALUES (temp.Field1, temp.Field2)
    OUTPUT temp.PracticeId, inserted.ReportOptionId, inserted.Field1, inserted.Field2
    INTO @PracticeReportOption (PracticeId, ReportOptionId, Field1, Field2);

Kluczem jest użycie predykatu, który nigdy nie będzie prawdziwy (1 = 0) w warunku wyszukiwania scalającego, więc zawsze będziesz wykonywać wstawianie, ale będziesz mieć dostęp do pól zarówno w tabeli źródłowej, jak i docelowej.


Oto cały kod, którego użyłem do przetestowania:

CREATE TABLE ReportOption (ReportOptionID INT IDENTITY(1, 1), Field1 INT, Field2 INT)
CREATE TABLE Practice (PracticeID INT IDENTITY(1, 1), Field1 INT, Field2 INT)
CREATE TABLE PracticeReportOption (PracticeReportOptionID INT IDENTITY(1, 1), PracticeID INT, ReportOptionID INT, Field1 INT, Field2 INT)

INSERT INTO Practice VALUES (1, 1), (2, 2), (3, 3), (4, 4)


MERGE INTO ReportOption r USING Practice p ON 1 = 0
WHEN NOT MATCHED THEN
    INSERT (field1, field2)
    VALUES (p.Field1, p.Field2)
    OUTPUT p.PracticeId, inserted.ReportOptionId, inserted.Field1, inserted.Field2
    INTO PracticeReportOption (PracticeId, ReportOptionId, Field1, Field2);

SELECT  *
FROM    PracticeReportOption

DROP TABLE ReportOption
DROP TABLE Practice
DROP TABLE PracticeReportOption 

Więcej lektur, a źródło wszystkiego, co wiem na ten temat, znajduje się tutaj

GarethD
źródło
2
Dzięki, to załatwia sprawę! Miałem zamiar użyć fałszywego pola tymczasowego, ale jest to znacznie bardziej eleganckie.
Alejandro B.
1
Doskonały! Ta sztuczka to ziarno złota! Dodano do pierwszego rzędu w kolekcji!
Vadim Loboda,
1
Suweet! Szkoda, że ​​nie musiał używać sporadycznie błędnego polecenia MERGE, ale poza tym jest idealnie elegancki.
Tab Alleman,
4
Być ostrzeżonym. Użyłem wyrażenia scalającego, które w ciągu ostatniego roku wzrosło wraz z użyciem. Zaczęliśmy mieć limity czasu podczas zapisywania i okazało się, że ponieważ instrukcja merge zawsze blokuje tabele, mieliśmy 35-160 sekund blokowania tabeli co 4 minuty. Muszę zrekonstruować kilka instrukcji scalania, aby używać wstawiania / aktualizacji i ograniczając liczbę wierszy, które aktualizują, do 500 na wstawienie / aktualizację, aby uniknąć blokowania tabeli. Szacuję, że ten bardzo ważny stół był zamknięty przez prawie 2 i pół godziny dziennie, co powodowało wszystko, od powolnych zapisów po przekroczenia limitów czasu.
CubeRoot
3
Ponadto, dla wielu przełomem jest to, że MERGE zawiera całą masę nie naprawionych błędów, które pojawiają się w dziwnych warunkach. Np. Zobacz artykuł Aarona Bertranda. Microsoft odmawia naprawienia niektórych z nich, a moje tajne podejrzenie jest takie, że MS wycofało całą swoją usługę MS Connect tylko po to, aby spróbować zmusić nas do zapomnienia o wszystkich tych błędach w oświadczeniu MERGE, których nie chcą naprawiać.
Reversed Engineer
14

Może ktoś, kto używa MS SQL Server 2005 lub starszego , uzna tę odpowiedź za przydatną.


MERGE będzie działać tylko dla SQL Server 2008 lub nowszego. Dla reszty znalazłem inne obejście, które da ci możliwość tworzenia tabel mapowania.

Oto jak będzie wyglądało rozwiązanie dla SQL 2005:

DECLARE @ReportOption TABLE (ReportOptionID INT IDENTITY(1, 1), Field1 INT, Field2 INT)
DECLARE @Practice TABLE(PracticeID INT IDENTITY(1, 1), Field1 INT, Field2 INT)
DECLARE @PracticeReportOption TABLE(PracticeReportOptionID INT IDENTITY(1, 1), PracticeID INT, ReportOptionID INT, Field1 INT, Field2 INT)

INSERT INTO @Practice (Field1, Field2) VALUES (1, 1)
INSERT INTO @Practice (Field1, Field2) VALUES (2, 2)
INSERT INTO @Practice (Field1, Field2) VALUES (3, 3)
INSERT INTO @Practice (Field1, Field2) VALUES (4, 4)

INSERT INTO @ReportOption (field1, field2)
    OUTPUT INSERTED.ReportOptionID, INSERTED.Field1, INSERTED.Field2 INTO @PracticeReportOption (ReportOptionID, Field1, Field2)
    SELECT Field1, Field2 FROM @Practice ORDER BY PracticeID ASC;


WITH CTE AS ( SELECT PracticeID, ROW_NUMBER() OVER ( ORDER BY PracticeID ASC ) AS ROW FROM @Practice )
UPDATE M SET M.PracticeID = S.PracticeID 
    FROM @PracticeReportOption AS M
    JOIN CTE AS S ON S.ROW = M.PracticeReportOptionID

    SELECT * FROM @PracticeReportOption

Główna sztuczka polega na tym, że dwukrotnie wypełniamy tabelę mapowania uporządkowanymi danymi z tabeli źródłowej i docelowej. Aby uzyskać więcej informacji, tutaj: Scalanie wstawionych danych przy użyciu OUTPUT w SQL Server 2005

Val
źródło
1
To nie rozwiązałoby mojego problemu. Potrzebuję Outputmoje Insertdane wyjściowe, Tablektóre zawierają Identitywartość z celu Tablez Insertwartością nieedytowaną (PK) ze źródła Table(przy okazji, więc mógłbym następnie (w innej partii) użyć tego wyjścia Tabledo wypełnienia Columnw źródło Tablez Identitywartością). Bez a Merge, wyobrażam sobie, że musiałbym: a) rozpocząć Transaction, b) przejść dalej Identity, c) wstawić do temp Tablez obliczoną Identity, d) ustawić Identity_Insert, e) Insertdo celu Tablez temp Table, f) wyczyścić Identity_Insert.
Tom