LEFT JOIN нескольких таблиц выдаёт дубликаты

596
29 декабря 2016, 07:33

Делаю билинговую систему к торговой интернет-площадке. В таблицах tbl_user - пользователи, tbl_billing_refill - пополнения баланса, tbl_billing - покупки/продажи (если id пользователя совпадает с buid - это его покупка, если с suid - это его продажа)

Текущая задача - правильно посчитать баланс для конкретного пользователя с id=1.

Упрощённые тестовые данные.

CREATE TABLE `tbl_user`
(
    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    `name` VARCHAR(32)
)
ENGINE=MyISAM CHARACTER SET utf8 COLLATE utf8_general_ci;
INSERT INTO `tbl_user` (`name`) VALUES ('Вася'), ('Петя'), ('Боря');

CREATE TABLE `tbl_billing_refill`
(
    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    `uid` INT UNSIGNED NOT NULL,
    `sum` DECIMAL(9,2) NOT NULL
)
ENGINE=MyISAM CHARACTER SET utf8 COLLATE utf8_general_ci;
INSERT INTO `tbl_billing_refill` (`uid`, `sum`) VALUES
(1, 1), (1, 10), (1, 100), (1, 100), (2, 2), (2, 20), (3, 300);

CREATE TABLE `tbl_billing`
(
    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    `buid` INT UNSIGNED NOT NULL,
    `suid` INT UNSIGNED NOT NULL,
    `sum` DECIMAL(9,2) UNSIGNED NOT NULL
)
ENGINE=MyISAM CHARACTER SET utf8 COLLATE utf8_general_ci;
INSERT INTO `tbl_billing` (`buid`,`suid`,`sum`) VALUES
(1, 3, 1), (1, 2, 10), (1, 2, 10), (1, 2, 100), (2, 1, 2), (2, 1, 2), (2, 1, 20), (3, 1, 300);

Запрос составил так:

1 Взял таблицу пользователей в качестве таблицы у которой гарантированно будут данные об id пользователя, что бы было к чему присоединять, а так мне данные из неё не нужны.

2 Мне нужен обязательно OUTER JOIN, так как любая из таблиц кроме tbl_user, в том числе и все сразу, могут не содержать данных об этом пользователе. Для этого же и использую IFNULL, чтобы NULL от таблицы с пустыми данными превратить в 0.

3 GROUP BY u.id включил, так как я использую агрегатную функцию SUM, которая требует, ну или раньше до 10.0 требовала, обязательной группировки. Запросы в MariaDB начиная с 10.0 уже выполняются и без неё, но ставлю по привычке.

Если владеете этим вопросом, разъясните правильно ли я поступаю?

4 Текущий баланс вычисляется так (сумма пополнений) - (сумма покупок) + (сумма продаж) Для пользователя с id=1 (1+10+100+100) - (1+10+10+100) + (2+2+20+300) = 211 - 121 + 324 = 414

SELECT (IFNULL(SUM(r.sum),0) - IFNULL(SUM(b.sum),0) + IFNULL(SUM(s.sum),0)) `sum`
FROM tbl_user `u`
LEFT JOIN tbl_billing_refill `r` ON u.id=r.uid
LEFT JOIN tbl_billing `b` ON u.id=b.buid
LEFT JOIN tbl_billing `s` ON u.id=s.suid
WHERE u.id=1 GROUP BY u.id;

Только вот проблема в том, что запрос вместо ожидаемых 414 выдаёт 6624 = 414*16, то есть одни и те же данные дублируются 16 раз.

Обычно, в подобных ситуациях, когда у меня COUNT(id), я ставил DISTINCT во все COUNT и всё работало как нужно. Я понимал, что я маскирую причину не устранив следствие, но причины я не понимал и устранить не мог. Вот теперь тот случай, когда в SUM(sum) DISTINCT уже не поставить, так как он будет резать нужные дубликаты, которые я специально включил в тестовые данные.

Помогите правильно и оптимально составить запрос и покажите как устранять эту причину, которую я всегда маскировал DISTINCT-ом, чтобы я больше так не делал. :-)

Answer 1

Нужно посчитать суммы по отдельности в подзапросах, и потом уже считать баланс.

SELECT rsum - bsum + ssum `sum`
FROM tbl_user `u`,
    (
    SELECT COALESCE(SUM(r.sum),0) rsum
    FROM tbl_billing_refill `r`
    WHERE r.uid=1
    ) q1,
    (
    SELECT COALESCE(SUM(b.sum),0) bsum
    FROM tbl_billing `b`
    WHERE b.buid=1
    ) q2,
    (
    SELECT COALESCE(SUM(s.sum),0) ssum
    FROM tbl_billing `s`
    WHERE s.suid=1
    ) q3
WHERE u.id=1

Впрочем, таблица tbl_user тут нафиг не нужна...

По просьбе alexoander вариант для нескольких юзеров:

SELECT u.id, q1.rsum - q2.bsum + q3.ssum `sum`
FROM tbl_user `u`,
    (
    SELECT r.uid id, COALESCE(SUM(r.sum),0) rsum
    FROM tbl_billing_refill `r`
    GROUP BY r.uid
    ) q1,
    (
    SELECT b.buid id, COALESCE(SUM(b.sum),0) bsum
    FROM tbl_billing `b`
    GROUP BY b.buid
    ) q2,
    (
    SELECT s.suid id, COALESCE(SUM(s.sum),0) ssum
    FROM tbl_billing `s`
    GROUP BY s.suid
    ) q3
WHERE u.id=q1.id
  AND u.id=q2.id
  AND u.id=q3.id
-- AND u.id IN (1,3,5,128)

Если нужно отбирать по небольшому количеству юзеров, имеет смысл добавить соотв. WHERE в подзапросы.

READ ALSO
Scroll к элементу

Scroll к элементу

У меня есть расписание на экранеНадо, чтобы по нажатию кнопки осуществлялся ScrollTo() либо ScrollBy() к элементу, но я не знаю, как определять координаты...

450
Один статический объект на два процесса

Один статический объект на два процесса

Как создать один статический объект на два процесса?

463
Как правьнее писать js скрипт в Сервлете

Как правьнее писать js скрипт в Сервлете

Есть JSP-файл с описанием HTML, в нем есть js-скрипты: ajax и все такое

441
Не проходит тест, ошибка No tests found matching Method

Не проходит тест, ошибка No tests found matching Method

Делаю тест для метода удаления данных

534