Как объединить самые новые строки из таблицы?
Вопрос
Я часто сталкиваюсь с проблемами такого типа и пока не нашел хорошего решения:
Предположим, у нас есть две таблицы базы данных, представляющие систему электронной коммерции.
userData (userId, name, ...)
orderData (orderId, userId, orderType, createDate, ...)
Для всех пользователей в системе выберите информацию о пользователе, самую последнюю информацию о заказе с типом = «1» и самую последнюю информацию о заказе с типом = «2».Я хочу сделать это в одном запросе.Вот пример результата:
(userId, name, ..., orderId1, orderType1, createDate1, ..., orderId2, orderType2, createDate2, ...)
(101, 'Bob', ..., 472, '1', '4/25/2008', ..., 382, '2', '3/2/2008', ...)
Решение
Это должно сработать, вам придется настроить имена таблиц/столбцов:
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')
Денормализация ваших данных также может быть хорошей идеей.Подобные работы будут стоить довольно дорого.Таким образом, вы можете добавить last_order_date
в ваши пользовательские данные.
Другие советы
Я предложил три разных подхода к решению этой проблемы:
- Использование опорных точек
- Использование операторов Case
- Использование встроенных запросов в предложенииwhere
Все решения предполагают, что мы определяем «самый последний» заказ на основе orderId
столбец.Используя createDate
столбец увеличил бы сложность из-за коллизий временных меток и серьезно снизил бы производительность, поскольку createDate
вероятно, не является частью индексированного ключа.Я тестировал эти запросы только с использованием MS SQL Server 2005, поэтому понятия не имею, будут ли они работать на вашем сервере.
Решения (1) и (2) работают практически одинаково.Фактически, оба они приводят к одинаковому количеству операций чтения из базы данных.
Решение (3) нет предпочтительный подход при работе с большими наборами данных.Он последовательно выполняет сотни логических операций чтения больше, чем (1) и (2).При фильтрации по одному конкретному пользователю подход (3) сопоставим с другими методами.В случае с одним пользователем снижение процессорного времени помогает компенсировать значительно большее количество операций чтения;однако по мере того, как диск становится более загруженным и возникают промахи в кэше, это небольшое преимущество исчезает.
Заключение
Для представленного сценария используйте поворотный подход, если он поддерживается вашей СУБД.Он требует меньше кода, чем оператор case, и упрощает добавление типов ордеров в будущем.
Обратите внимание, что в некоторых случаях PIVOT недостаточно гибок, и лучше всего использовать функции значений характеристик, использующие операторы Case.
Код
Подход (1) с использованием 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
Подход (2) с использованием операторов случая:
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
Подход (3) с использованием встроенных запросов в предложенииwhere (на основе ответа Стива К.):
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)
Скрипт для генерации таблиц и 1000 пользователей по 100 заказов каждый:
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
Небольшой фрагмент для тестирования производительности запросов на MS SQL Server в дополнение к 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
Извините, у меня нет оракула передо мной, но это базовая структура того, что я бы сделал в оракуле:
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 (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)
В SQL 2005 вы также можете использовать функцию RANK ( ) OVER.(Но, AFAIK, это полностью специфичная для MSSQL функция)
Для этого вы можете выполнить запрос на объединение.Точный синтаксис требует доработки, особенно группировка по разделам, но объединение должно это сделать.
Например:
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
Их новейшие вы имеете в виду все новые в текущий день?Вы всегда можете проверить свою дату createDate и получить все данные о пользователях и заказах, если createDate >= текущий день.
SELECT * FROM
"orderData", "userData"
WHERE
"userData"."userId" ="orderData"."userId"
AND "orderData".createDate >= current_date;
ОБНОВЛЕНО
Вот что вы хотите после вашего комментария здесь:
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
)
я использую такие вещи в 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
Короче говоря, используйте MAX(), чтобы получить новейшие данные, добавив поле критериев (createDate) к интересующим полям (otherfield).SUBSTRING_INDEX() затем удаляет дату.
OTOH, если вам нужно произвольное количество заказов (если userType может быть любым числом, а не ограниченным ENUM);лучше обрабатывать отдельным запросом, примерно так:
select * from orderData where userId=XXX order by orderType, date desc group by orderType
для каждого пользователя.
Предполагая, что orderId монотонно увеличивается со временем:
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
Затем поверните клиент или, если вы используете SQL Server, есть функция PIVOT.
Вот один из способов переместить данные типа 1 и 2 в одну строку:
(путем помещения информации типа 1 и типа 2 в отдельные элементы выбора, которые затем используются в предложении from.)
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
Вот как я это делаю.Это стандартный SQL, который работает в базе данных любой марки.
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);
Обратите внимание: если у вас есть несколько заказов любого типа, даты которых совпадают с последней датой, вы получите несколько строк в наборе результатов.Если у вас есть несколько заказов обоих типов, в наборе результатов вы получите N x M строк.Поэтому я бы рекомендовал вам получать строки каждого типа в отдельных запросах.
Стив К. абсолютно прав, спасибо!Я немного переписал его ответ, чтобы учесть тот факт, что для определенного типа может не быть заказа (о котором я не упомянул, поэтому не могу винить Стива К.)
Вот что я в итоге использовал:
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]...;