Wie die neuesten Zeilen aus einer Tabelle verbinden?
Frage
Ich betreibe häufig Probleme dieser Form und haben keine gute Lösung noch nicht gefunden:
Angenommen, wir haben zwei Datenbanktabellen repräsentieren ein E-Commerce-System.
userData (userId, name, ...)
orderData (orderId, userId, orderType, createDate, ...)
Für alle Benutzer im System, wählen Sie ihre Benutzerinformationen, die neuesten Informationen, um mit type = ‚1‘, und ihre jüngste Bestellung Informationen mit type = ‚2‘. Ich möchte dies in einer Abfrage tun. Hier ist ein Beispiel Ergebnis:
(userId, name, ..., orderId1, orderType1, createDate1, ..., orderId2, orderType2, createDate2, ...)
(101, 'Bob', ..., 472, '1', '4/25/2008', ..., 382, '2', '3/2/2008', ...)
Lösung
Das sollte funktionieren, müssen Sie die Tabelle / Spaltennamen einstellen müssen, um:
select ud.name,
order1.order_id,
order1.order_type,
order1.create_date,
order2.order_id,
order2.order_type,
order2.create_date
from user_data ud,
order_data order1,
order_data order2
where ud.user_id = order1.user_id
and ud.user_id = order2.user_id
and order1.order_id = (select max(order_id)
from order_data od1
where od1.user_id = ud.user_id
and od1.order_type = 'Type1')
and order2.order_id = (select max(order_id)
from order_data od2
where od2.user_id = ud.user_id
and od2.order_type = 'Type2')
Ihre Daten Denormalisierung könnte auch eine gute Idee sein. Diese Art der Sache wird ziemlich teuer sein zu tun. So könnten Sie eine last_order_date
zu Ihrem Userdata hinzuzufügen.
Andere Tipps
Ich habe zur Lösung dieses Problems drei verschiedene Ansätze zur Verfügung gestellt:
- Verwenden von Pivots
- Verwenden von Case-Anweisungen
- Verwenden von Inline-Abfragen in der where-Klausel
Alle Lösungen übernehmen wir die „jüngsten“, um auf der orderId
Säule basieren bestimmen. Mit Hilfe der createDate
Spalte Komplexität fügt hinzu, aufgrund Zeitstempel Kollisionen und ernsthaft die Leistung beeinträchtigen, da createDate
wahrscheinlich nicht Teil der indizierten Schlüssel. Ich habe nur diese Abfragen mit MS SQL Server 2005 getestet, so habe ich keine Ahnung, ob sie auf dem Server arbeiten.
Lösungen (1) und (2) erfüllt fast identisch. In der Tat, sie beide Ergebnis in der gleichen Anzahl von liest aus der Datenbank.
Lösung (3) nicht die bevorzugte Lösung, wenn mit großen Datenmengen arbeiten. Es macht konsequent Hunderte von logischen lesen mehr als (1) und (2). Wenn für einen bestimmten Benutzer Filterung, Ansatz (3) mit den anderen Verfahren vergleichbar. Im Single-User-Fall hilft ein Tropfen auf der CPU-Zeit, die deutlich höhere Anzahl von liest entgegenzuwirken; jedoch, wie das Plattenlaufwerk auftritt Misses belebte und Cache wird dieser leichte Vorteil verschwinden.
Fazit
Für das präsentierte Szenario verwenden, um den Pivot-Ansatz, wenn sie von Ihrem DBMS unterstützt wird. Es erfordert weniger Code als die Case-Anweisung und vereinfacht die Auftragsarten in der Zukunft hinzuzufügen.
Bitte beachten Sie, dass in einigen Fällen ist PIVOT nicht flexibel genug und Kennwertfunktionen Fall-Anweisungen sind der Weg zu gehen.
Code
Ansatz (1) mit PIVOT:
select
ud.userId, ud.fullname,
od1.orderId as orderId1, od1.createDate as createDate1, od1.orderType as orderType1,
od2.orderId as orderId2, od2.createDate as createDate2, od2.orderType as orderType2
from userData ud
inner join (
select userId, [1] as typeOne, [2] as typeTwo
from (select
userId, orderType, orderId
from orderData) as orders
PIVOT
(
max(orderId)
FOR orderType in ([1], [2])
) as LatestOrders) as LatestOrders on
LatestOrders.userId = ud.userId
inner join orderData od1 on
od1.orderId = LatestOrders.typeOne
inner join orderData od2 on
od2.orderId = LatestOrders.typeTwo
Ansatz (2) mit Case-Anweisungen:
select
ud.userId, ud.fullname,
od1.orderId as orderId1, od1.createDate as createDate1, od1.orderType as orderType1,
od2.orderId as orderId2, od2.createDate as createDate2, od2.orderType as orderType2
from userData ud
-- assuming not all users will have orders use outer join
inner join (
select
od.userId,
-- can be null if no orders for type
max (case when orderType = 1
then ORDERID
else null
end) as maxTypeOneOrderId,
-- can be null if no orders for type
max (case when orderType = 2
then ORDERID
else null
end) as maxTypeTwoOrderId
from orderData od
group by userId) as maxOrderKeys on
maxOrderKeys.userId = ud.userId
inner join orderData od1 on
od1.ORDERID = maxTypeTwoOrderId
inner join orderData od2 on
OD2.ORDERID = maxTypeTwoOrderId
Ansatz (3) mit Inline-Abfragen in der Where-Klausel (basierend auf Steve K. Antwort):
select ud.userId,ud.fullname,
order1.orderId, order1.orderType, order1.createDate,
order2.orderId, order2.orderType, order2.createDate
from userData ud,
orderData order1,
orderData order2
where ud.userId = order1.userId
and ud.userId = order2.userId
and order1.orderId = (select max(orderId)
from orderData od1
where od1.userId = ud.userId
and od1.orderType = 1)
and order2.orderId = (select max(orderId)
from orderData od2
where od2.userId = ud.userId
and od2.orderType = 2)
Script zu generieren Tabellen und 1000 Benutzer mit 100 Bestellungen pro:
CREATE TABLE [dbo].[orderData](
[orderId] [int] IDENTITY(1,1) NOT NULL,
[createDate] [datetime] NOT NULL,
[orderType] [tinyint] NOT NULL,
[userId] [int] NOT NULL
)
CREATE TABLE [dbo].[userData](
[userId] [int] IDENTITY(1,1) NOT NULL,
[fullname] [nvarchar](50) NOT NULL
)
-- Create 1000 users with 100 order each
declare @userId int
declare @usersAdded int
set @usersAdded = 0
while @usersAdded < 1000
begin
insert into userData (fullname) values ('Mario' + ltrim(str(@usersAdded)))
set @userId = @@identity
declare @orderSetsAdded int
set @orderSetsAdded = 0
while @orderSetsAdded < 10
begin
insert into orderData (userId, createDate, orderType)
values ( @userId, '01-06-08', 1)
insert into orderData (userId, createDate, orderType)
values ( @userId, '01-02-08', 1)
insert into orderData (userId, createDate, orderType)
values ( @userId, '01-08-08', 1)
insert into orderData (userId, createDate, orderType)
values ( @userId, '01-09-08', 1)
insert into orderData (userId, createDate, orderType)
values ( @userId, '01-01-08', 1)
insert into orderData (userId, createDate, orderType)
values ( @userId, '01-06-06', 2)
insert into orderData (userId, createDate, orderType)
values ( @userId, '01-02-02', 2)
insert into orderData (userId, createDate, orderType)
values ( @userId, '01-08-09', 2)
insert into orderData (userId, createDate, orderType)
values ( @userId, '01-09-01', 2)
insert into orderData (userId, createDate, orderType)
values ( @userId, '01-01-04', 2)
set @orderSetsAdded = @orderSetsAdded + 1
end
set @usersAdded = @usersAdded + 1
end
Kleiner Schnipsel zum Testen der Abfrageleistung auf MS SQL Server zusätzlich zu SQL Profiler:
-- Uncomment these to clear some caches
--DBCC DROPCLEANBUFFERS
--DBCC FREEPROCCACHE
set statistics io on
set statistics time on
-- INSERT TEST QUERY HERE
set statistics time off
set statistics io off
Leider habe ich nicht Orakel vor mir, aber das ist die Grundstruktur von dem, was ich in Oracle tun würde:
SELECT b.user_id, b.orderid, b.orderType, b.createDate, <etc>,
a.name
FROM orderData b, userData a
WHERE a.userid = b.userid
AND (b.userid, b.orderType, b.createDate) IN (
SELECT userid, orderType, max(createDate)
FROM orderData
WHERE orderType IN (1,2)
GROUP BY userid, orderType)
T-SQL Probenlösung (MS SQL):
SELECT
u.*
, o1.*
, o2.*
FROM
(
SELECT
, userData.*
, (SELECT TOP 1 orderId.url FROM orderData WHERE orderData.userId=userData.userId AND orderType=1 ORDER BY createDate DESC)
AS order1Id
, (SELECT TOP 1 orderId.url FROM orderData WHERE orderData.userId=userData.userId AND orderType=2 ORDER BY createDate DESC)
AS order2Id
FROM userData
) AS u
LEFT JOIN orderData o1 ON (u.order1Id=o1.orderId)
LEFT JOIN orderData o2 ON (u.order2Id=o2.orderId)
In SQL 2005 Sie könnten auch RANK () OVER-Funktion verwenden. (Aber AFAIK seine vollständig MSSQL spezifische Funktion)
Sie könnten in der Lage sein, eine Union-Abfrage für dies zu tun. Die genaue Syntax braucht etwas Arbeit, vor allem die Gruppe durch den Abschnitt, aber die Union sollte es tun können.
Zum Beispiel:
SELECT orderId, orderType, createDate
FROM orderData
WHERE type=1 AND MAX(createDate)
GROUP BY orderId, orderType, createDate
UNION
SELECT orderId, orderType, createDate
FROM orderData
WHERE type=2 AND MAX(createDate)
GROUP BY orderId, orderType, createDate
Ihre neueste meinen Sie alle neu in den aktuellen Tag? Sie können jederzeit mit Ihrem ErstellDat überprüfen und alle Benutzer- und Auftragsdaten, wenn die ErstellDat> = aktuellen Tag.
SELECT * FROM
"orderData", "userData"
WHERE
"userData"."userId" ="orderData"."userId"
AND "orderData".createDate >= current_date;
AKTUALISIERT
Hier ist, was Sie nach Ihrem Kommentar mögen hier:
SELECT * FROM
"orderData", "userData"
WHERE
"userData"."userId" ="orderData"."userId"
AND "orderData".type = '1'
AND "orderData"."orderId" = (
SELECT "orderId" FROM "orderData"
WHERE
"orderType" = '1'
ORDER "orderId" DESC
LIMIT 1
)
Ich benutze Dinge wie diese in MySQL:
SELECT
u.*,
SUBSTRING_INDEX( MAX( CONCAT( o1.createDate, '##', o1.otherfield)), '##', -1) as o2_orderfield,
SUBSTRING_INDEX( MAX( CONCAT( o2.createDate, '##', o2.otherfield)), '##', -1) as o2_orderfield
FROM
userData as u
LEFT JOIN orderData AS o1 ON (o1.userId=u.userId AND o1.orderType=1)
LEFT JOIN orderData AS o2 ON (o1.userId=u.userId AND o2.orderType=2)
GROUP BY u.userId
Kurz gesagt, verwenden Sie MAX (), um die neueste zu erhalten, durch Voranstellung des Kriterienfeld (ErstellDat) auf den interessanten Bereich (e) (otherfield). SUBSTRING_INDEX () abstreift dann das Datum an.
OTOH, wenn Sie eine beliebige Anzahl von Aufträgen müssen (wenn usertype eine beliebige Zahl sein kann, und nicht eine begrenzte ENUM); es besser ist, mit einer separaten Abfrage, so etwas wie das zu handhaben:
select * from orderData where userId=XXX order by orderType, date desc group by orderType
für jeden Benutzer.
orderId Unter der Annahme, monoton mit zunehmender Zeit:
SELECT *
FROM userData u
INNER JOIN orderData o
ON o.userId = u.userId
INNER JOIN ( -- This subquery gives the last order of each type for each customer
SELECT MAX(o2.orderId)
--, o2.userId -- optional - include if joining for a particular customer
--, o2.orderType -- optional - include if joining for a particular type
FROM orderData o2
GROUP BY o2.userId
,o2.orderType
) AS LastOrders
ON LastOrders.orderId = o.orderId -- expand join to include customer or type if desired
Dann schwenkt den Client an oder bei Verwendung von SQL Server gibt es eine PIVOT Funktionalität
Hier ist ein Weg, den Typen 1 und 2 Daten auf die gleiche Zeile zu bewegen:
(Durch den Typ 1 und Typ-2-Informationen in ihre eigenen wählt platzieren, die dann in der FROM-Klausel gewöhnen.)
SELECT
a.name, ud1.*, ud2.*
FROM
userData a,
(SELECT user_id, orderid, orderType, reateDate, <etc>,
FROM orderData b
WHERE (userid, orderType, createDate) IN (
SELECT userid, orderType, max(createDate)
FROM orderData
WHERE orderType = 1
GROUP BY userid, orderType) ud1,
(SELECT user_id, orderid, orderType, createDate, <etc>,
FROM orderData
WHERE (userid, orderType, createDate) IN (
SELECT userid, orderType, max(createDate)
FROM orderData
WHERE orderType = 2
GROUP BY userid, orderType) ud2
Hier ist, wie ich es tun. Dies ist Standard-SQL und arbeitet in jeder Marke von Datenbank.
SELECT u.userId, u.name, o1.orderId, o1.orderType, o1.createDate,
o2.orderId, o2.orderType, o2.createDate
FROM userData AS u
LEFT OUTER JOIN (
SELECT o1a.orderId, o1a.userId, o1a.orderType, o1a.createDate
FROM orderData AS o1a
LEFT OUTER JOIN orderData AS o1b ON (o1a.userId = o1b.userId
AND o1a.orderType = o1b.orderType AND o1a.createDate < o1b.createDate)
WHERE o1a.orderType = 1 AND o1b.orderId IS NULL) AS o1 ON (u.userId = o1.userId)
LEFT OUTER JOIN (
SELECT o2a.orderId, o2a.userId, o2a.orderType, o2a.createDate
FROM orderData AS o2a
LEFT OUTER JOIN orderData AS o2b ON (o2a.userId = o2b.userId
AND o2a.orderType = o2b.orderType AND o2a.createDate < o2b.createDate)
WHERE o2a.orderType = 2 AND o2b.orderId IS NULL) o2 ON (u.userId = o2.userId);
Beachten Sie, dass, wenn Sie mehrere Aufträge beiden Typen, deren Termine sind gleich den letzten Termin haben, werden Sie mehrere Zeilen in der Ergebnismenge erhalten. Wenn Sie mehrere Aufträge beiden Typen haben, werden Sie M N x Zeilen in der Ergebnismenge erhalten. Deshalb würde ich empfehlen, dass Sie die Zeilen jeder Art in separaten Abfragen zu holen.
Steve K ist absolut richtig, danke! Ich habe seine Antwort ein wenig umschreiben für die Tatsache zu berücksichtigen, dass es vielleicht nicht, um für einen bestimmten Typ sein (was ich nicht erwähnt zu, so kann ich nicht bemängeln Steve K.)
Hier ist, was ich in Liquidation mit:
select ud.name,
order1.orderId,
order1.orderType,
order1.createDate,
order2.orderId,
order2.orderType,
order2.createDate
from userData ud
left join orderData order1
on order1.orderId = (select max(orderId)
from orderData od1
where od1.userId = ud.userId
and od1.orderType = '1')
left join orderData order2
on order2.orderId = (select max(orderId)
from orderData od2
where od2.userId = ud.userId
and od2.orderType = '2')
where ...[some limiting factors on the selection of users]...;