Ausgehend von der folgenden Tabelle in SQL Server 2005:
ID Col1 Col2 Col3
-- ---- ---- ----
1 3 34 76
2 32 976 24
3 7 235 3
4 245 1 792
Was ist der beste Weg, um die Abfrage zu schreiben, die das folgende Ergebnis liefert (d. H. Eine, die die letzte Spalte liefert - eine Spalte mit den Mindestwerten aus Col1, Col2 und Col 3 für jede Zeile)?
ID Col1 Col2 Col3 TheMin
-- ---- ---- ---- ------
1 3 34 76 3
2 32 976 24 24
3 7 235 3 3
4 245 1 792 1
UPDATE:
Zur Verdeutlichung (wie ich in den Kommentaren gesagt habe) ist die Datenbank im realen Szenario richtig normalisiert. Diese "Array" -Spalten befinden sich nicht in einer tatsächlichen Tabelle, sondern in einer Ergebnismenge, die in einem Bericht erforderlich ist. Neu ist, dass der Bericht auch diese MinValue-Spalte benötigt. Ich kann die zugrunde liegende Ergebnismenge nicht ändern und habe daher nach einem praktischen "Get out of Jail Card" für T-SQL gesucht.
Ich habe den unten erwähnten CASE-Ansatz ausprobiert und er funktioniert, obwohl er etwas umständlich ist. Es ist auch komplizierter als in den Antworten angegeben, da Sie die Tatsache berücksichtigen müssen, dass sich zwei min-Werte in derselben Zeile befinden.
Wie auch immer, ich dachte, ich würde meine aktuelle Lösung posten, die unter Berücksichtigung meiner Einschränkungen ziemlich gut funktioniert. Es verwendet den UNPIVOT-Operator:
with cte (ID, Col1, Col2, Col3)
as
(
select ID, Col1, Col2, Col3
from TestTable
)
select cte.ID, Col1, Col2, Col3, TheMin from cte
join
(
select
ID, min(Amount) as TheMin
from
cte
UNPIVOT (Amount for AmountCol in (Col1, Col2, Col3)) as unpvt
group by ID
) as minValues
on cte.ID = minValues.ID
Ich sage vorweg, dass ich nicht erwarte, dass dies die beste Leistung bietet, aber unter den gegebenen Umständen (ich kann nicht alle Abfragen nur für die neue MinValue-Spaltenanforderung umgestalten) ist es ein ziemlich elegantes "Get out of Jail" Karte".
Es gibt wahrscheinlich viele Möglichkeiten, dies zu erreichen. Mein Vorschlag ist, Case/When zu verwenden, um es zu tun. Mit 3 Spalten ist es nicht so schlimm.
Select Id,
Case When Col1 < Col2 And Col1 < Col3 Then Col1
When Col2 < Col1 And Col2 < Col3 Then Col2
Else Col3
End As TheMin
From YourTableNameHere
Mit CROSS APPLY
:
SELECT ID, Col1, Col2, Col3, MinValue
FROM YourTable
CROSS APPLY (SELECT MIN(d) MinValue FROM (VALUES (Col1), (Col2), (Col3)) AS a(d)) A
SELECT ID, Col1, Col2, Col3,
(SELECT MIN(Col) FROM (VALUES (Col1), (Col2), (Col3)) AS X(Col)) AS TheMin
FROM Table
Der beste Weg dies zu tun ist wahrscheinlich nicht es zu tun - es ist seltsam, dass die Leute darauf bestehen, ihre Daten auf eine Weise zu speichern, die SQL "Gymnastik" erfordert, um sinnvolle Informationen zu extrahieren, wenn es weitaus einfacher ist Möglichkeiten, um das gewünschte Ergebnis zu erzielen, wenn Sie Ihr Schema nur ein wenig besser strukturieren :-)
Die richtig Möglichkeit, dies zu tun, besteht meiner Meinung nach darin, die folgende Tabelle zu haben:
ID Col Val
-- --- ---
1 1 3
1 2 34
1 3 76
2 1 32
2 2 976
2 3 24
3 1 7
3 2 235
3 3 3
4 1 245
4 2 1
4 3 792
mit ID/Col
als Primärschlüssel (und möglicherweise Col
als zusätzlichen Schlüssel, abhängig von Ihren Anforderungen). Dann wird Ihre Abfrage zu einer einfachen select min(val) from tbl
und Sie können die einzelnen 'alten Spalten' weiterhin separat behandeln, indem Sie in Ihren anderen Abfragen where col = 2
Verwenden. Dies ermöglicht auch eine einfache Erweiterung, falls die Anzahl der "alten Spalten" zunimmt.
Dies macht Ihre Abfragen so viel einfacher. Die allgemeine Richtlinie, die ich normalerweise verwende, lautet: Wenn Sie ever etwas haben, das wie ein Array in einer Datenbankzeile aussieht, machen Sie wahrscheinlich etwas falsch und sollten über eine Umstrukturierung der Daten nachdenken.
Wenn Sie jedoch aus irgendeinem Grund kann nicht diese Spalten ändern, würde ich vorschlagen, Einfüge- und Aktualisierungs-Trigger zu verwenden und eine andere Spalte hinzuzufügen, auf die diese Trigger eingestellt sind das Minimum auf Col1/2/3
. Dadurch werden die 'Kosten' des Vorgangs von der Auswahl zu dem Update/Insert verschoben, zu dem er gehört - die meisten Datenbanktabellen werden nach meiner Erfahrung weitaus häufiger gelesen als geschrieben, sodass die Kosten für das Schreiben mit der Zeit tendenziell effizienter sind.
Mit anderen Worten, das Minimum für eine Zeile ändert sich nur, wenn sich eine der anderen Spalten ändert, also das ist wenn Sie es berechnen sollten, nicht jedes Mal, wenn Sie auswählen (was verschwendet wird, wenn die Daten nicht vorhanden sind nicht ändern). Sie würden dann mit einem Tisch enden wie:
ID Col1 Col2 Col3 MinVal
-- ---- ---- ---- ------
1 3 34 76 3
2 32 976 24 24
3 7 235 3 3
4 245 1 792 1
Jede andere Option, die zum Zeitpunkt select
Entscheidungen treffen muss, ist in der Regel eine schlechte Idee, da sich die Daten nur beim Einfügen/Aktualisieren ändern - das Hinzufügen einer weiteren Spalte nimmt mehr Platz in der Datenbank in Anspruch und wird dies auch sein etwas langsamer für die Einfügungen und Aktualisierungen, kann aber für Auswahlen viel schneller sein - der bevorzugte Ansatz sollte von Ihren Prioritäten dort abhängen, aber, wie angegeben, werden die meisten Tabellen gelesen weit öfter als geschrieben.
Sie können den "Brute Force" -Ansatz mit einer Wendung verwenden:
SELECT CASE
WHEN Col1 <= Col2 AND Col1 <= Col3 THEN Col1
WHEN Col2 <= Col3 THEN Col2
ELSE Col3
END AS [Min Value] FROM [Your Table]
Wenn die erste when-Bedingung fehlschlägt, wird garantiert, dass Col1 nicht der kleinste Wert ist. Sie können ihn daher aus den übrigen Bedingungen entfernen. Ebenso für nachfolgende Bedingungen. Für fünf Spalten wird Ihre Abfrage zu:
SELECT CASE
WHEN Col1 <= Col2 AND Col1 <= Col3 AND Col1 <= Col4 AND Col1 <= Col5 THEN Col1
WHEN Col2 <= Col3 AND Col2 <= Col4 AND Col2 <= Col5 THEN Col2
WHEN Col3 <= Col4 AND Col3 <= Col5 THEN Col3
WHEN Col4 <= Col5 THEN Col4
ELSE Col5
END AS [Min Value] FROM [Your Table]
Beachten Sie, dass bei einem Gleichstand zwischen zwei oder mehr Spalten <=
stellt sicher, dass wir die Anweisung CASE
so früh wie möglich beenden.
Benutze das:
select least(col1, col2, col3) FROM yourtable
Wenn die Spalten Ganzzahlen wären, wie in Ihrem Beispiel, würde ich eine Funktion erstellen:
create function f_min_int(@a as int, @b as int)
returns int
as
begin
return case when @a < @b then @a else coalesce(@b,@a) end
end
dann, wenn ich es benutzen muss, würde ich tun:
select col1, col2, col3, dbo.f_min_int(dbo.f_min_int(col1,col2),col3)
wenn Sie 5 Spalten haben, wird das oben genannte
select col1, col2, col3, col4, col5,
dbo.f_min_int(dbo.f_min_int(dbo.f_min_int(dbo.f_min_int(col1,col2),col3),col4),col5)
Sie können dies auch mit einer Unionsabfrage tun. Wenn die Anzahl der Spalten zunimmt, müsste die Abfrage geändert werden, zumindest ist dies eine einfache Änderung.
Select T.Id, T.Col1, T.Col2, T.Col3, A.TheMin
From YourTable T
Inner Join (
Select A.Id, Min(A.Col1) As TheMin
From (
Select Id, Col1
From YourTable
Union All
Select Id, Col2
From YourTable
Union All
Select Id, Col3
From YourTable
) As A
Group By A.Id
) As A
On T.Id = A.Id
Das ist rohe Gewalt, funktioniert aber
select case when col1 <= col2 and col1 <= col3 then col1
case when col2 <= col1 and col2 <= col3 then col2
case when col3 <= col1 and col3 <= col2 then col3
as 'TheMin'
end
from Table T
... weil min () nur für eine Spalte und nicht spaltenübergreifend funktioniert.
Beide diese Frage Und diese Frage versuchen dies zu beantworten.
Die Zusammenfassung ist, dass Oracle eine eingebaute Funktion dafür hat. Mit SQL Server stecken Sie fest, entweder eine benutzerdefinierte Funktion zu definieren oder case-Anweisungen zu verwenden.
Wenn Sie in der Lage sind, eine gespeicherte Prozedur zu erstellen, kann dies ein Array von Werten annehmen, und Sie können dies einfach aufrufen.
select *,
case when column1 < columnl2 And column1 < column3 then column1
when columnl2 < column1 And columnl2 < column3 then columnl2
else column3
end As minValue
from tbl_example
SELECT [ID],
(
SELECT MIN([value].[MinValue])
FROM
(
VALUES
([Col1]),
([Col1]),
([Col2]),
([Col3])
) AS [value] ([MinValue])
) AS [MinValue]
FROM Table;
Wenn Sie SQL 2005 verwenden, können Sie etwas Ordentliches tun:
;WITH res
AS ( SELECT t.YourID ,
CAST(( SELECT Col1 AS c01 ,
Col2 AS c02 ,
Col3 AS c03 ,
Col4 AS c04 ,
Col5 AS c05
FROM YourTable AS cols
WHERE YourID = t.YourID
FOR
XML AUTO ,
ELEMENTS
) AS XML) AS colslist
FROM YourTable AS t
)
SELECT YourID ,
colslist.query('for $c in //cols return min(data($c/*))').value('.',
'real') AS YourMin ,
colslist.query('for $c in //cols return avg(data($c/*))').value('.',
'real') AS YourAvg ,
colslist.query('for $c in //cols return max(data($c/*))').value('.',
'real') AS YourMax
FROM res
Auf diese Weise verlieren Sie sich nicht in so vielen Operatoren :)
Dies könnte jedoch langsamer sein als die andere Wahl.
Es ist deine Wahl...
Für mehrere Spalten empfiehlt es sich, eine CASE-Anweisung zu verwenden. Für zwei numerische Spalten i und j können Sie jedoch einfache Mathematik verwenden:
min (i, j) = (i + j)/2 - abs (i-j)/2
Diese Formel kann verwendet werden, um den Mindestwert für mehrere Spalten zu erhalten, aber es ist wirklich chaotisch, dass nach 2 min (i, j, k) min (i, min (j, k)) ist.
Unten benutze ich eine temporäre Tabelle, um das Minimum von mehreren Daten zu erhalten. Die erste temporäre Tabelle fragt mehrere verknüpfte Tabellen ab, um verschiedene Daten (sowie andere Werte für die Abfrage) zu erhalten. Die zweite temporäre Tabelle ruft dann die verschiedenen Spalten und das Mindestdatum mit so vielen Durchläufen ab, wie es Datumsspalten gibt.
Dies ist im Wesentlichen wie bei der Unionsabfrage, die gleiche Anzahl von Durchläufen ist erforderlich, kann jedoch effizienter sein (basierend auf der Erfahrung, müsste jedoch getestet werden). Effizienz war in diesem Fall kein Problem (8.000 Datensätze). Man könnte etc. indizieren.
--==================== this gets minimums and global min
if object_id('tempdb..#temp1') is not null
drop table #temp1
if object_id('tempdb..#temp2') is not null
drop table #temp2
select r.recordid , r.ReferenceNumber, i.InventionTitle, RecordDate, i.ReceivedDate
, min(fi.uploaddate) [Min File Upload], min(fi.CorrespondenceDate) [Min File Correspondence]
into #temp1
from record r
join Invention i on i.inventionid = r.recordid
left join LnkRecordFile lrf on lrf.recordid = r.recordid
left join fileinformation fi on fi.fileid = lrf.fileid
where r.recorddate > '2015-05-26'
group by r.recordid, recorddate, i.ReceivedDate,
r.ReferenceNumber, i.InventionTitle
select recordid, recorddate [min date]
into #temp2
from #temp1
update #temp2
set [min date] = ReceivedDate
from #temp1 t1 join #temp2 t2 on t1.recordid = t2.recordid
where t1.ReceivedDate < [min date] and t1.ReceivedDate > '2001-01-01'
update #temp2
set [min date] = t1.[Min File Upload]
from #temp1 t1 join #temp2 t2 on t1.recordid = t2.recordid
where t1.[Min File Upload] < [min date] and t1.[Min File Upload] > '2001-01-01'
update #temp2
set [min date] = t1.[Min File Correspondence]
from #temp1 t1 join #temp2 t2 on t1.recordid = t2.recordid
where t1.[Min File Correspondence] < [min date] and t1.[Min File Correspondence] > '2001-01-01'
select t1.*, t2.[min date] [LOWEST DATE]
from #temp1 t1 join #temp2 t2 on t1.recordid = t2.recordid
order by t1.recordid
Ein kleiner Dreh in der Gewerkschaftsabfrage:
DECLARE @Foo TABLE (ID INT, Col1 INT, Col2 INT, Col3 INT)
INSERT @Foo (ID, Col1, Col2, Col3)
VALUES
(1, 3, 34, 76),
(2, 32, 976, 24),
(3, 7, 235, 3),
(4, 245, 1, 792)
SELECT
ID,
Col1,
Col2,
Col3,
(
SELECT MIN(T.Col)
FROM
(
SELECT Foo.Col1 AS Col UNION ALL
SELECT Foo.Col2 AS Col UNION ALL
SELECT Foo.Col3 AS Col
) AS T
) AS TheMin
FROM
@Foo AS Foo
Wenn Sie wissen, nach welchen Werten Sie suchen, normalerweise nach einem Statuscode, kann Folgendes hilfreich sein:
select case when 0 in (PAGE1STATUS ,PAGE2STATUS ,PAGE3STATUS,
PAGE4STATUS,PAGE5STATUS ,PAGE6STATUS) then 0 else 1 end
FROM CUSTOMERS_FORMS
Ich weiß, dass diese Frage alt ist, aber ich war immer noch auf der Suche nach einer Antwort und war mit anderen Antworten nicht zufrieden. Deshalb musste ich mir eine eigene Frage ausdenken, die eine Wendung von @ paxdiablo´s answer darstellt.
Ich stamme aus dem Land von SAP ASE 16.0 und benötigte nur einen Blick auf die Statistik bestimmter Daten, die IMHO in verschiedenen Spalten einer einzelnen Zeile gültig gespeichert sind (sie stellen unterschiedliche Zeiten dar - als die Ankunft von etwas geplant war, was wann erwartet wurde) die aktion hat begonnen und endlich was war die aktuelle zeit). So hatte ich Spalten in die Zeilen der temporären Tabelle transponiert und meine Abfrage darüber wie gewohnt durchgeführt.
N.B. Nicht die Einheitslösung voraus!
CREATE TABLE #tempTable (ID int, columnName varchar(20), dataValue int)
INSERT INTO #tempTable
SELECT ID, 'Col1', Col1
FROM sourceTable
WHERE Col1 IS NOT NULL
INSERT INTO #tempTable
SELECT ID, 'Col2', Col2
FROM sourceTable
WHERE Col2 IS NOT NULL
INSERT INTO #tempTable
SELECT ID, 'Col3', Col3
FROM sourceTable
WHERE Col3 IS NOT NULL
SELECT ID
, min(dataValue) AS 'Min'
, max(dataValue) AS 'Max'
, max(dataValue) - min(dataValue) AS 'Diff'
FROM #tempTable
GROUP BY ID
Dies dauerte etwa 30 Sekunden bei einer Quellmenge von 630000 Zeilen und verwendete nur Indexdaten. Dies ist also nicht die Voraussetzung für die Ausführung in einem zeitkritischen Prozess, sondern für Dinge wie die einmalige Datenüberprüfung oder den Tagesabschlussbericht in Ordnung (aber überprüfen Sie dies bitte mit Ihren Kollegen oder Vorgesetzten!). Der Hauptbonus dieses Stils für mich war, dass ich leicht mehr/weniger Spalten verwenden und die Gruppierung, Filterung usw. ändern konnte, insbesondere wenn die Daten kopiert wurden.
Die zusätzlichen Daten (columnName
, max
es, ...) sollten mir bei meiner Suche helfen, sodass Sie sie möglicherweise nicht benötigen. Ich habe sie hier gelassen, um vielleicht spark einige Ideen :-).