I have a project with users, placed in some kind of hierarchy tree. a user can have multiple children and multiple parents. the organization chart looks like this :
this hierarchy uses 2 tables in database, user and position. here is the SQL to reproduce the tables with few fixtures to reproduce the organization image provided below:
-- phpMyAdmin SQL Dump
-- version 4.8.0
-- https://www.phpmyadmin.net/
--
-- Hôte : db
-- Généré le : jeu. 24 mai 2018 à 07:39
-- Version du serveur : 10.2.14-MariaDB-10.2.14+maria~jessie
-- Version de PHP : 7.2.4
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET AUTOCOMMIT = 0;
START TRANSACTION;
SET time_zone = "+00:00";
CREATE DATABASE IF NOT EXISTS `hierarchy` DEFAULT CHARACTER SET latin1 COLLATE latin1_swedish_ci;
USE `hierarchy`;
DROP TABLE IF EXISTS `position`;
CREATE TABLE `position` (
`id` int(11) NOT NULL,
`user_id` int(11) NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`uuid` char(36) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '(DC2Type:guid)',
`type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `position` (`id`, `user_id`, `parent_id`, `uuid`, `type`) VALUES
(2, 3, NULL, '7db54d6a-6a89-3fe2-87ba-c0dd96d4f664', 'TYPE_HR_PLUS'),
(3, 4, 3, 'adadab97-c0b8-3f89-a245-4edb077a4579', 'TYPE_HR_PLUS'),
(4, 5, 3, '4efec484-8a1f-3c27-a48f-a8c8412e18db', 'TYPE_HR_PLUS'),
(5, 6, 4, '8027b0cb-3e34-357a-8bff-80e4abbf60ef', 'TYPE_HR_PLUS'),
(6, 7, 4, 'e588ec20-9b0e-3ec6-8164-bdfafdf4b440', 'TYPE_HR'),
(7, 8, 5, '61e537d7-5ed6-36b3-8969-1913cdacb42b', 'TYPE_HR'),
(8, 9, 5, '6c241f8f-1a0a-34dc-8042-591352a764ea', 'TYPE_HR_PLUS'),
(9, 9, 3, 'e03998c4-d95a-3410-9727-099f0c83b3d5', 'TYPE_COLLABORATOR'),
(10, 10, 6, '67de2883-a7b0-3260-bb2a-67d3ffa3dc40', 'TYPE_HR'),
(11, 11, 7, '07d37612-01bf-3eec-b249-41c20d756144', 'TYPE_COLLABORATOR'),
(12, 11, 5, '09f7f6f1-59e2-37c0-a2e4-ae9b58ba6c93', 'TYPE_HR'),
(13, 12, 9, '724b8185-b6eb-3d35-b71f-1d076b3020ed', 'TYPE_HR'),
(14, 13, 9, '97d90e5c-7872-3e1e-be65-0f6b5329f54c', 'TYPE_HR_PLUS'),
(15, 14, 10, '3c357b9d-e162-316b-921c-25157d523e70', 'TYPE_COLLABORATOR'),
(16, 14, 3, '542235c6-462c-4922-a629-303430701000', 'TYPE_COLLABORATOR'),
(17, 15, 10, '2d208936-a7e9-32c1-963f-0df7f57ae463', 'TYPE_COLLABORATOR'),
(18, 16, 11, '00b678a2-54cd-3f37-9381-0cd92acea079', 'TYPE_COLLABORATOR'),
(19, 17, 8, 'ec4b8cad-fbce-3692-8d4c-f48f7ffa6452', 'TYPE_COLLABORATOR'),
(20, 18, 12, '48f0c85c-3801-3275-8978-5b2573fd7e0b', 'TYPE_COLLABORATOR'),
(21, 18, 10, 'd6129dec-8823-3304-8dfe-819118073d1f', 'TYPE_COLLABORATOR'),
(22, 19, 13, '1ec9c202-97d7-3311-9119-f6e9f55d058b', 'TYPE_COLLABORATOR'),
(23, 20, 9, '3ab32120-520b-3d41-80f0-56fd3876eecb', 'TYPE_COLLABORATOR');
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL,
`uuid` char(36) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '(DC2Type:guid)',
`first_name` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
`last_name` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `user` (`id`, `uuid`, `first_name`, `last_name`) VALUES
(3, 'fb7304f6-5ade-4c06-83e5-36827a4d7904', 'Albus', 'Dumbledore'),
(4, '60af55ca-e9cd-3d8a-bc8a-c9f7ec99baa1', 'Minerva', 'McGonagall'),
(5, 'a422b364-b68f-324b-8836-d38ca62001f5', 'Severus', 'Rogue'),
(6, 'cbdf94a8-f4e3-360b-ba7f-10ea7e59b85d', 'Filius', 'Flitwick'),
(7, '9bce88c9-7b7d-3db3-8780-b6397bb1263d', 'Remus', 'Lupin'),
(8, 'b9d85cfa-3c46-33c1-8750-53be1c281111', 'Pomona', 'Chourave'),
(9, 'c07d10d7-333f-327a-ab32-201209127b7a', 'Rolanda', 'Bibine'),
(10, '8a7b76a1-5f0c-31ff-a93a-ca478598682b', 'Gilderoy', 'Lockhart'),
(11, '7302700a-0bc5-3ed0-84fc-857e975a5771', 'Alastor', 'Maugrey'),
(12, '7b8722f3-1570-3351-864a-ede9ee3a90e6', 'Sybille', 'Trelawney'),
(13, '12a7683a-00da-46a6-b7fd-a6eae5b10e3c', 'Dolores', 'Ombrage'),
(14, 'f054bdeb-a8b8-41e5-be78-b00d1b9377f3', 'Harry', 'Potter'),
(15, 'b1f696ad-cfa3-3830-8685-c45ff4a4ffda', 'Ronald', 'Weasley'),
(16, '8b2a64e9-ed28-363d-a731-3e15e255cd1d', 'Hermione', 'Granger'),
(17, 'f6aa74f9-2a4a-346d-bf75-3f8b02614c19', 'Neville', 'Londubat'),
(18, '8fbbc431-2555-3718-8055-ad8637260732', 'Drago', 'Malefoy'),
(19, '78a58546-8ae1-330d-9426-c23c74b71e78', 'Luna', 'Lovegood'),
(20, '4739a44b-d1b7-3390-9bbb-cbd2e39b4ec0', 'Ginny', 'Weasley');
ALTER TABLE `position`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `uuid_idx` (`uuid`),
ADD KEY `IDX_462CE4F5727ACA70` (`parent_id`),
ADD KEY `IDX_462CE4F5A76ED395` (`user_id`);
ALTER TABLE `user`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `uuid_idx` (`uuid`);
ALTER TABLE `position`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1024;
ALTER TABLE `user`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1027;
ALTER TABLE `position`
ADD CONSTRAINT `FK_462CE4F5727ACA70` FOREIGN KEY (`parent_id`) REFERENCES `user` (`id`),
ADD CONSTRAINT `FK_462CE4F5A76ED395` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`);
COMMIT;
One important thing to notice is the position table represents a relation between a user and its parent, and it also stores a string defining the relation type (actually it represents the user type of the child in this relation).
EDIT : one really important thing I didn't mention yet is there is no recursivity in this tree, there can be only a maximum number of 5 levels, as shown in the organization image: 1 HR_PLUS Head of Department, followed by maximum 3 levels of HR_PLUS or HR, followed by 1 level of COLLAB
What I'm trying to accomplish is the following:
For a given position uuid, i'd like to fetch every child user below this relation.
For example, the relation between Minerva McGonagall and Albus Dumbledore is the position #3 with uuid adadab97-c0b8-3f89-a245-4edb077a4579. Given this uuid, i'd like to fetch every user below Minerva McGonagall:
+---------------------------------------+
| id | first_name | last_name |
+---------------------------------------+
| 6 | Filius | Flitwick |
| 7 | Remus | Lupin |
| 10 | Gilderoy | Lockhart |
| 11 | Alastor | Maugrey |
| 14 | Harry | Potter |
| 15 | Ronald | Weasley |
| 16 | Hermione | Granger |
| 18 | Drago | Malefoy |
+---------------------------------------+
The query I wrote so far is the following:
SELECT u2.id, u2.first_name, u2.last_name
FROM `user` u1
JOIN position p1 ON p1.user_id = u1.id
JOIN position p2 ON p2.parent_id = p1.user_id
JOIN user u2 ON u2.id = p2.user_id
WHERE p1.uuid = 'adadab97-c0b8-3f89-a245-4edb077a4579'
AND p1.type IN ('TYPE_HR', 'TYPE_HR_PLUS')
I added a filter on type property because I don't want the query to return anything if type is not HR or HR_PLUS.
Right now, the query returns the users directly below Minerva McGonagall (Filius and Remus). And I can't figure how to return the rest of the users below them.
any help would be greatly appreciated.
I just succeed in fetch the expected data using a UNION, as this is not a recursive tree:
SELECT u2.id, u2.first_name, u2.last_name
FROM `user` u1
JOIN position p1 ON p1.user_id = u1.id
JOIN position p2 ON p2.parent_id = p1.user_id
JOIN user u2 ON u2.id = p2.user_id
WHERE p1.uuid = 'adadab97-c0b8-3f89-a245-4edb077a4579'
AND p1.type IN ('TYPE_HR', 'TYPE_HR_PLUS')
UNION
SELECT u2.id, u2.first_name, u2.last_name
FROM `user` u1
JOIN position p1 ON p1.user_id = u1.id
JOIN position p2 ON p2.parent_id = p1.user_id
JOIN position p3 ON p3.parent_id = p2.user_id
JOIN user u2 ON u2.id = p3.user_id
WHERE p1.uuid = 'adadab97-c0b8-3f89-a245-4edb077a4579'
AND p1.type IN ('TYPE_HR', 'TYPE_HR_PLUS')
UNION
SELECT u2.id, u2.first_name, u2.last_name
FROM `user` u1
JOIN position p1 ON p1.user_id = u1.id
JOIN position p2 ON p2.parent_id = p1.user_id
JOIN position p3 ON p3.parent_id = p2.user_id
JOIN position p4 ON p4.parent_id = p3.user_id
JOIN user u2 ON u2.id = p4.user_id
WHERE p1.uuid = 'adadab97-c0b8-3f89-a245-4edb077a4579'
AND p1.type IN ('TYPE_HR', 'TYPE_HR_PLUS')
but this looks very heavy , and i'm pretty sure there is a simpler way to do it.
Since you have already found the union solution, here's an alternative using a loop (And pseudocode):
Create a table to hold your results, lets call it r.
Insert into r the results of your first query, along with a field named level with a value of 1.
set i=2
while (at least a line of r exists where level=i-1) do the following:
insert into r (level=i)
a modification of your select based on selecting positions from r
where level=i-1
Is this what you're looking for? DB Fiddle
with recursive cte as (
select `user_id` as `ancestor_id`
/* , 0 as `degreesOfSeperation` */
, `user_id`
from `position`
union all
select c.`ancestor_id`
/* , c.`degreesOfSeperation` + 1 */
, p.`user_id`
from `cte` as c
inner join `position` as p
on p.`parent_id` = c.`user_id`
)
select u.id, u.first_name, u.last_name
from cte c
inner join `user` a on a.`id` = c.`ancestor_id`
inner join `position` p on p.`user_id` = a.`id`
inner join `user` u on u.`id` = c.`user_id`
/* WHERE c.`degreesOfSeperation` > 0 */
WHERE c.`user_id` != c.`ancestor_id`
AND p.`uuid` = 'adadab97-c0b8-3f89-a245-4edb077a4579'
AND p.`type` IN ('TYPE_HR', 'TYPE_HR_PLUS')
order by c.`degreesOfSeperation`
;
Output
id first_name last_name
Id | First_Name | Last_Name
---+------------+----------
6 | Filius | Flitwick
7 | Remus | Lupin
10 | Gilderoy | Lockhart
11 | Alastor | Maugrey
18 | Drago | Malefoy
14 | Harry | Potter
15 | Ronald | Weasley
16 | Hermione | Granger
Most of this is likely already already familiar to you.
The logic to deal with the hierarchical structure is the recursive cte.
This has 2 parts:
Part 1
select user_id as ancestor_id
, 0 as degreesOfSeperation
, user_id
from position
This gets all users from the position table, setting their record as the ancestor record; i.e. the root from which we'll grow our tree. We set degreesOfSeperation to 0 to state that this is the root record; i.e. the one where the user and the ancestor are one and the same.
Part 2
select c.`ancestor_id`
, c.`degreesOfSeperation` + 1
, p.`user_id`
from `cte` as c
inner join `position` as p
on p.`parent_id` = c.`user_id`
This is the recursive part; for each user added to the tree already (i.e. from the root statement; or from the last iteration of populating the cte table) we now fetch any other positions for which the position's parents match the previous iteration's user. We add one to the degreesOfSeparation so we can tell how far away from the root ancestor this record is.
DegressOfSeparation is not actually required (in fact, in the above statement I've commented this out); but I tend to include it since it's useful for debugging / seeing what's going on. Here's what the cte looks like without any filters, to help you understand what's going on:
With recursive cte as (
select `user_id` as `ancestor_id`
, 0 as `degreesOfSeperation`
, `user_id`
from `position`
union all
select c.`ancestor_id`
, c.`degreesOfSeperation` + 1
, p.`user_id`
from `cte` as c
inner join `position` as p
on p.`parent_id` = c.`user_id`
)
select *
from cte c
order by c.`ancestor_id`
, c.`degreesOfSeperation`
, c.`user_id`
;
Results here
Related
I need to join actual document to people. Documents has date issued (passport, for example).
SQL Fiddle: http://sqlfiddle.com/#!9/3a8118/2/0
Table structure:
CREATE TABLE people
(
p_id INT NOT NULL AUTO_INCREMENT,
p_name VARCHAR(50) NOT NULL,
PRIMARY KEY(p_id)
);
INSERT INTO people (p_id, p_name)
VALUES (1, 'Name_1'),
(2, 'Name_2');
CREATE TABLE documents
(
d_id INT NOT NULL AUTO_INCREMENT,
d_people INT(10) NOT NULL,
d_date VARCHAR(10) NOT NULL,
PRIMARY KEY(d_id)
);
INSERT INTO documents (d_id, d_people, d_date)
VALUES (1, 1, '01.01.2022'),
(2, 2, '01.12.2021'),
(3, 1, '05.02.2022'),
(4, 1, '10.02.2022'),
(5, 2, '04.01.2022'),
(6, 1, '20.01.2022');
Query: condition is select actual document when date is 21.01.2022, it must return d_id = 6:
SELECT *
FROM people
INNER JOIN documents ON d_people = p_id
WHERE p_id = 1 AND ??? d_date 21.01.2022 ???
;
I need to do an inner join to return only this row:
use this query:
Fiddle
SELECT * FROM people
INNER JOIN documents ON d_people = p_id
WHERE p_id = 1 and d_date='20.01.2022';
In an informix database, I have a hierarchical tree structure, where a child entry can have any level of parents (parent, grandparent, etc). via relation to its parent entry.
Every entry has a collection of attribute names and values.
The tables are modeled is as follows:
node:
+-------------+----------------------+--------+
| id | parn_id | name |
+-------------+----------------------+--------+
| int | int | string |
| primary key | existing id, or null | |
+-------------+----------------------+--------+
vals:
+-----------------------+-------------+---------+
| id | atr_id | atr_val |
+-----------------------+-------------+---------+
| int | int | string |
| foreign key from node | primary key | |
+-----------------------+-------------+---------+
look:
+-----------------------+--------+
| atr_id | name |
+-----------------------+--------+
| int | string |
| foreign key from vals | |
+-----------------------+--------+
I need a sql query which returns all of the parent's (vals, look) pairs when asking for a child.
For example, if I have
Parent: (valP, nameP), (valP2, nameP2)
*
* * * Child (valC, nameC)
*
* * * GrandChild (valGC, nameGC)
And I query for the GrandChild, I want it to return:
GrandChild (valP, nameP), (valP2, nameP2), (valC, nameC), (valGC, nameGC)
Using a recent Informix version ( I am using Informix 14.10.FC1 ) you can use the CONNECT BY clause to work with hierarchical queries.
The setup, based on your description:
CREATE TABLE node
(
id INTEGER PRIMARY KEY,
parn_id INTEGER,
name CHAR( 20 ) NOT NULL
);
INSERT INTO node VALUES ( 1, NULL, 'Node_A' );
INSERT INTO node VALUES ( 2, NULL, 'Node_B' );
INSERT INTO node VALUES ( 3, NULL, 'Node_C' );
INSERT INTO node VALUES ( 4, 2, 'Node_D' );
INSERT INTO node VALUES ( 5, 3, 'Node_E' );
INSERT INTO node VALUES ( 6, 3, 'Node_F' );
INSERT INTO node VALUES ( 7, 4, 'Node_G' );
CREATE TABLE vals
(
id INTEGER NOT NULL REFERENCES node( id ),
atr_id INTEGER PRIMARY KEY,
atr_val CHAR( 20 ) NOT NULL
);
INSERT INTO vals VALUES ( 1, 1, 'Value_A_1' );
INSERT INTO vals VALUES ( 2, 2, 'Value_B_1' );
INSERT INTO vals VALUES ( 2, 3, 'Value_B_2' );
INSERT INTO vals VALUES ( 3, 4, 'Value_C_1' );
INSERT INTO vals VALUES ( 3, 5, 'Value_C_2' );
INSERT INTO vals VALUES ( 4, 6, 'Value_D_1' );
INSERT INTO vals VALUES ( 5, 7, 'Value_E_1' );
INSERT INTO vals VALUES ( 5, 8, 'Value_E_2' );
INSERT INTO vals VALUES ( 6, 9, 'Value_F_1' );
INSERT INTO vals VALUES ( 7, 10, 'Value_G_1' );
CREATE TABLE look
(
atr_id INTEGER NOT NULL REFERENCES vals( atr_id ),
name CHAR( 20 ) NOT NULL
);
INSERT INTO look VALUES ( 1, 'Look_A_1' );
INSERT INTO look VALUES ( 2, 'Look_B_1' );
INSERT INTO look VALUES ( 3, 'Look_B_2' );
INSERT INTO look VALUES ( 4, 'Look_C_1' );
INSERT INTO look VALUES ( 5, 'Look_C_2' );
INSERT INTO look VALUES ( 6, 'Look_D_1' );
INSERT INTO look VALUES ( 7, 'Look_E_1' );
INSERT INTO look VALUES ( 8, 'Look_E_2' );
INSERT INTO look VALUES ( 9, 'Look_F_1' );
INSERT INTO look VALUES ( 10, 'Look_G_1' );
we can use the CONNECT BY to find the child parents, for example:
-- Starting from 'Node_G'
SELECT
n.id,
n.parn_id,
n.name,
CONNECT_BY_ROOT n.name AS starting_node
FROM
node AS n
START WITH n.name = 'Node_G'
CONNECT BY PRIOR n.parn_id = n.id
ORDER BY
n.name
;
-- RESULTS:
id parn_id name starting_node
2 Node_B Node_G
4 2 Node_D Node_G
7 4 Node_G Node_G
And then we can join with the attribute tables:
SELECT
vt1.starting_node,
v.atr_val,
l.name
FROM
(
SELECT
n.id,
n.parn_id,
n.name,
CONNECT_BY_ROOT n.name AS starting_node
FROM
node AS n
START WITH n.name = 'Node_G'
CONNECT BY PRIOR n.parn_id = n.id
) AS vt1
INNER JOIN vals AS v
ON
v.id = vt1.id
INNER JOIN look AS l
ON
l.atr_id = v.atr_id
ORDER BY
vt1.starting_node, v.atr_val
;
-- RESULTS:
starting_node atr_val name
Node_G Value_B_1 Look_B_1
Node_G Value_B_2 Look_B_2
Node_G Value_D_1 Look_D_1
Node_G Value_G_1 Look_G_1
If we remove the START WITH clause, we get the hierarchical results for each node:
SELECT
vt1.starting_node,
v.atr_val,
l.name
FROM
(
SELECT
n.id,
n.parn_id,
n.name,
CONNECT_BY_ROOT n.name AS starting_node
FROM
node AS n
CONNECT BY PRIOR n.parn_id = n.id
) AS vt1
INNER JOIN vals AS v
ON
v.id = vt1.id
INNER JOIN look AS l
ON
l.atr_id = v.atr_id
ORDER BY
vt1.starting_node, v.atr_val
;
-- RESULTS:
starting_node atr_val name
Node_A Value_A_1 Look_A_1
Node_B Value_B_1 Look_B_1
Node_B Value_B_2 Look_B_2
Node_C Value_C_1 Look_C_1
Node_C Value_C_2 Look_C_2
Node_D Value_B_1 Look_B_1
Node_D Value_B_2 Look_B_2
Node_D Value_D_1 Look_D_1
Node_E Value_C_1 Look_C_1
Node_E Value_C_2 Look_C_2
Node_E Value_E_1 Look_E_1
Node_E Value_E_2 Look_E_2
Node_F Value_C_1 Look_C_1
Node_F Value_C_2 Look_C_2
Node_F Value_F_1 Look_F_1
Node_G Value_B_1 Look_B_1
Node_G Value_B_2 Look_B_2
Node_G Value_D_1 Look_D_1
Node_G Value_G_1 Look_G_1
I am quite new to SQL and I am having some difficulty with my COUNT() feature.
It keeps returning the wrong COUNT() value as I am trying to calculate the TOTAL number of specific cars sold by users. My tables and results are here:
http://sqlfiddle.com/#!9/d2ef0/5
My Schema:
CREATE TABLE IF NOT EXISTS Users(
userID INT NOT NULL AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
forename VARCHAR(50) NOT NULL,
surname VARCHAR(50) NOT NULL,
PRIMARY KEY (userID)
);
CREATE TABLE IF NOT EXISTS CarType(
carTypeID INT NOT NULL AUTO_INCREMENT,
description VARCHAR(80),
PRIMARY KEY (carTypeID)
);
CREATE TABLE IF NOT EXISTS Country(
countryID INT NOT NULL AUTO_INCREMENT,
name VARCHAR(100),
PRIMARY KEY (countryID)
);
CREATE TABLE IF NOT EXISTS Cars(
carID INT NOT NULL AUTO_INCREMENT,
carTypeID INT NOT NULL,
countryID INT NOT NULL,
description VARCHAR(100) NOT NULL,
make VARCHAR(100) NOT NULL,
model VARCHAR(100),
FOREIGN KEY (carTypeID) REFERENCES CarType(carTypeID),
FOREIGN KEY (countryID) REFERENCES Country(countryID),
PRIMARY KEY (carID)
);
CREATE TABLE IF NOT EXISTS Likes(
userID INT NOT NULL,
carID INT NOT NULL,
likes DOUBLE NOT NULL,
FOREIGN KEY (userID) REFERENCES Users(userID),
FOREIGN KEY (carID) REFERENCES Cars(carID)
);
CREATE TABLE IF NOT EXISTS Sold(
userID INT NOT NULL,
carID INT NOT NULL,
FOREIGN KEY (userID) REFERENCES Users(userID),
FOREIGN KEY (carID) REFERENCES Cars(carID)
);
INSERT INTO Users VALUES
(NULL, "micheal", "Micheal", "Sco"),
(NULL, "bensco", "Ben", "Sco"),
(NULL, "shanemill", "Shane", "Miller");
INSERT INTO CarType VALUES
(NULL, "Saloon"),
(NULL, "HatchBack"),
(NULL, "Low Rider");
INSERT INTO Country VALUES
(NULL, "UK"),
(NULL, "USA"),
(NULL, "JAPAN"),
(NULL, "GERMANY");
INSERT INTO Cars VALUES
(NULL, 1, 2, "Ford Mustang lovers", "Mustang", "Ford"),
(NULL, 2, 3, "Drift Kings", "Skyline", "Nissan"),
(NULL, 3, 1, "British classic", "Cooper", "Mini");
INSERT INTO Likes VALUES
(1, 1, 3),
(1, 2, 2),
(2, 3, 5),
(2, 3, 7),
(2, 3, 1),
(2, 3, 2);
INSERT INTO Sold VALUES
(1, 2),
(1, 3),
(1, 1),
(2, 2),
(2, 3),
(3, 1),
(3, 3);
This is the Sold table:
userID carID
1 2
1 3
1 1
2 2
2 3
3 1
3 3
This is my complex query:
SELECT DISTINCT Cars.carID, Cars.description, Cars.model, Country.name,
CarType.description, ROUND(AVG(Likes.likes)), COUNT(*)
FROM Cars
INNER JOIN Sold ON
Cars.carID = Sold.carID
INNER JOIN Country ON
Cars.countryID = Country.countryID
INNER JOIN CarType ON
Cars.carTypeID = CarType.carTypeID
INNER JOIN Likes ON
Cars.carID = Likes.carID
GROUP BY Cars.carID
The actual result from this complex SQL Query:
carID description model name description ROUND(AVG(Likes.likes)) COUNT(*)
1 Ford Mustang lovers Ford USA Saloon 3 2
2 Drift Kings Nissan JAPAN HatchBack 2 2
3 British classic Mini UK Low Rider 4 12
For example, the result for the last one is incorrect - it should not be 12
Would be nice if someone could tell me where I went wrong
Thanks
You are attempting to aggregate across two different dimensions -- Sold and Likes. The result is a Cartesian product of rows for each car, and this throws off the aggregations.
The solution is the pre-aggregate the results along each dimension:
SELECT c.carID, c.description, c.model, cy.name, ct.description,
l.avgLikes, s.NumSold
FROM Cars c INNER JOIN
(SELECT s.CarId, COUNT(*) as NumSold
FROM Sold s
GROUP BY s.CarId
) s
ON c.carID = s.carID INNER JOIN
Country cy
ON c.countryID = cy.countryID INNER JOIN
CarType ct
ON c.carTypeID = ct.carTypeID LEFT JOIN
(SELECT l.carId, AVG(Likes) as avgLikes
FROM Likes l
GROUP BY CarId
) l
ON c.carID = l.carID;
Here is the SQL Fiddle.
If all you want is the total number of specific cars sold by user then all your info is in the sold table. This query will give you what you want by carID. You can use that as a subquery if you want to join on other tables to get more info.
SELECT userID, carID, count(*) as totalSold FROM Sold GROUP BY userID, carID;
I have trouble with using Arel to aggregate 2 columns in the same query. When I run this, the whole server freezes for a minute, before the rails dev-server crashes. I suspect an infinite loop :).
Maybe I have misunderstood the whole concept of Arel, and I would be grateful if anybody could have a look at it.
The expected result of this query is something like this:
[{:user_id => 1, :sum_account_charges => 300, :sum_paid_debts => 1000},...]
a_account_charges = Table(:account_charges)
a_paid_debts = Table(:paid_debts)
a_participants = Table(:expense_accounts_users)
account_charge_sum = a_account_charges
.where(a_account_charges[:expense_account_id].eq(id))
.group(a_account_charges[:user_id])
.project(a_account_charges[:user_id], a_account_charges[:cost].sum)
paid_debts_sum = a_paid_debts
.where(a_paid_debts[:expense_account_id].eq(id))
.group(a_paid_debts[:from_user_id])
.project(a_paid_debts[:from_user_id], a_paid_debts[:cost].sum)
charges = a_participants
.where(a_participants[:expense_account_id].eq(id))
.join(account_charge_sum)
.on(a_participants[:user_id].eq(account_charge_sum[:user_id]))
.join(paid_debts_sum)
.on(a_participants[:user_id].eq(paid_debts_sum[:from_user_id]))
I'm new to arel, but after banging on this for several days and really digging, I don't think it can be done. Here's an outline of what I've done, if anyone has any additional insight it would be welcome.
First, these scripts will create the test tables and populate them with test data. I have set up 9 expense_account_users, each with a different set of charges/paid_debts, as follows: 1 charge/1 payment, 2 charges/2 payments, 2 charges/1 payment, 2 charges/0 payments, 1 charge/2 payments, 0 charges/2 payments, 1 charge/0 payments, 0 charges/1 payment, 0 charges, 0 payments.
CREATE TABLE IF NOT EXISTS `expense_accounts_users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`expense_account_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=10 ;
INSERT INTO `expense_accounts_users` (`id`, `expense_account_id`) VALUES (1, 1), (2, 1), (3, 1), (4, 1), (5, 1), (6, 1), (7, 1), (8, 1), (9, 1);
CREATE TABLE IF NOT EXISTS `account_charges` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`expense_account_id` int(11) DEFAULT NULL,
`user_id` int(11) DEFAULT NULL,
`cost` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=10 ;
INSERT INTO `account_charges` (`id`, `expense_account_id`, `user_id`, `cost`) VALUES (1, 1, 1, 1), (2, 1, 2, 1), (3, 1, 2, 2), (4, 1, 3, 1), (5, 1, 3, 2), (6, 1, 4, 1), (7, 1, 5, 1), (8, 1, 5, 2), (9, 1, 7, 1);
CREATE TABLE IF NOT EXISTS `paid_debts` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`expense_account_id` int(11) DEFAULT NULL,
`user_id` int(11) DEFAULT NULL,
`cost` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=10 ;
INSERT INTO `paid_debts` (`id`, `expense_account_id`, `user_id`, `cost`) VALUES (1, 1, 1, 1), (2, 1, 2, 1), (3, 1, 2, 2), (4, 1, 3, 1), (5, 1, 4, 1), (6, 1, 4, 2), (7, 1, 6, 1), (8, 1, 6, 2), (9, 1, 8, 1);
Ultimately, to get the data that you are after in one fell swoop, this is the SQL statement you'd use:
SELECT user_charges.user_id,
user_charges.sum_cost,
COALESCE(SUM(paid_debts.cost), 0) AS 'sum_paid'
FROM (
SELECT expense_accounts_users.id AS 'user_id',
COALESCE(sum(account_charges.cost), 0) AS 'sum_cost'
FROM expense_accounts_users
LEFT OUTER JOIN account_charges on expense_accounts_users.id = account_charges.user_id
GROUP BY expense_accounts_users.id)
AS user_charges
LEFT OUTER JOIN paid_debts ON user_charges.user_id = paid_debts.user_id
GROUP BY user_charges.user_id
You have to do a LEFT OUTER JOIN between users and charges first so that you get a row for every user, then you have to LEFT OUTER JOIN the result to debts in order to avoid multiplying your results with the two joins inside the same construct.
(note the use of COALESCE to convert NULL values from the LEFT OUTER JOINs to zeroes - a convenience item, perhaps)
The result of this statement is this:
user_id sum_cost sum_paid
1 1 1
2 3 3
3 3 1
4 1 3
5 3 0
6 0 3
7 1 0
8 0 1
9 0 0
After many attempts, I found that this arel code came the closest to what we are after:
c = Arel::Table.new(:account_charges)
d = Arel::Table.new(:paid_debts)
p = Arel::Table.new(:expense_accounts_users)
user_charges = p
.where(p[:expense_account_id].eq(1))
.join(c, Arel::Nodes::OuterJoin)
.on(p[:id].eq(c[:user_id]))
.project(p[:id], c[:cost].sum.as('sum_cost'))
.group(p[:id])
charges = user_charges
.join(d, Arel::Nodes::OuterJoin)
.on(p[:id].eq(d[:user_id]))
.project(d[:cost].sum.as('sum_paid'))
Essentially, I am joining users to charges in the first construct with a LEFT OUTER JOIN, then trying to take the result of that and LEFT OUTER JOIN it back to debts. This arel code produces the following SQL statement:
SELECT `expense_accounts_users`.`id`,
SUM(`account_charges`.`cost`) AS sum_cost,
SUM(`paid_debts`.`cost`) AS sum_paid
FROM `expense_accounts_users`
LEFT OUTER JOIN `account_charges` ON `expense_accounts_users`.`id` = `account_charges`.`user_id`
LEFT OUTER JOIN `paid_debts` ON `expense_accounts_users`.`id` = `paid_debts`.`user_id`
WHERE `expense_accounts_users`.`expense_account_id` = 1
GROUP BY `expense_accounts_users`.`id`
Which, when run, produces this output:
id sum_cost sum_paid
1 1 1
2 6 6
3 3 2
4 2 3
5 3 NULL
6 NULL 3
7 1 NULL
8 NULL 1
9 NULL NULL
Very close, but not quite. First, the lack of COALESCE gives us NULL values instead of zeroes - I'm not sure how to effect the COALESCE function call from within arel.
More importantly, though, combining the LEFT OUTER JOINs into one statement without the internal subselect is resulting in the sum_paid totals getting multiplied up in examples 2, 3 and 4 - any time there is more than one of either charge or payment and at least one of the other.
Based on some online readings, I had hoped that changing the arel slightly would solve the problem:
charges = user_charges
.join(d, Arel::Nodes::OuterJoin)
.on(user_charges[:id].eq(d[:user_id]))
.project(d[:cost].sum.as('sum_paid'))
But any time I used user_charges[] in the second arel construct, I got an undefined method error for SelectManager#[]. This may be a bug, or may be right - I really can't tell.
I just don't see that arel has a way to make use of the SQL from the first construct as a queryable object in the second construct with the necessary subquery alias, as is needed to make this happen in one SQL statement.
I have 2 tables. One is 'page_links' and the other is 'rpp'. Table page_links is the superset of table rpp.
The following is the schema of my tables:
-- Table structure for table `page_links`
--
CREATE TABLE IF NOT EXISTS `page_links` (
`page` varchar(255) NOT NULL,
`page_link` varchar(100) NOT NULL,
`heading_id` tinyint(3) unsigned NOT NULL,
PRIMARY KEY (`page`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Dumping data for table `page_links`
--
INSERT INTO `page_links` (`page`, `page_link`, `heading_id`) VALUES
('a1.php', 'A1', 8),
('b1.php', 'B1', 8),
('c1.php', 'C1', 5),
('d1.php', 'D1', 5),
('e1.php', 'E1', 8),
('f1.php', 'F1', 8),
('g1.php', 'G1', 8),
('h1.php', 'H1', 1),
('i1.php', 'I1', 1),
('j1.php', 'J1', 8),
('k1.php', 'K1', 8),
('l1.php', 'L1', 8),
('m1.php', 'M1', 8),
('n1.php', 'N1', 8),
('o1.php', 'O1', 8),
('p1.php', 'P1', 4),
('q1.php', 'Q1', 5),
('r1.php', 'R1', 4);
-- Table structure for table `rpp`
--
CREATE TABLE IF NOT EXISTS `rpp` (
`role_id` tinyint(3) unsigned NOT NULL,
`page` varchar(255) NOT NULL,
`is_allowed` tinyint(1) NOT NULL,
PRIMARY KEY (`role_id`,`page`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Dumping data for table `rpp`
--
INSERT INTO `rpp` (`role_id`, `page`, `is_allowed`) VALUES
(3, 'a1.php', 1),
(3, 'b1.php', 1),
(3, 'c1.php', 1),
(3, 'd1.php', 1),
(3, 'e1.php', 1),
(3, 'f1.php', 1),
(3, 'h1.php', 1),
(3, 'i1.php', 1),
(3, 'l1.php', 1),
(3, 'm1.php', 1),
(3, 'n1.php', 1),
(4, 'a1.php', 1),
(4, 'b1.php', 1),
(4, 'q1.php', 1),
(5, 'r1.php', 1);
WHAT I AM TRYING TO DO:
I am trying to query both the above tables (in a single query) in such a way that all the pages from page_links are displayed along with the is_allowed value from rpp for a particular role. For example, I want to get the is_allowed value of all the pages from rpp for role_id = 3 and at the same time, list all the available pages from page_links. A clear example of my expected result would be:
page is_allowed role_id
----------------------------------------
a1.php 1 3
b1.php 1 3
c1.php 1 3
d1.php 1 3
e1.php 1 3
f1.php 1 3
g1.php NULL NULL
h1.php 1 3
i1.php 1 3
j1.php NULL NULL
k1.php NULL NULL
l1.php 1 3
m1.php 1 3
n1.php 1 3
o1.php NULL NULL
p1.php NULL NULL
q1.php NULL NULL
r1.php NULL NULL
One more example of my desired result could be achieved by doing a LEFT JOIN rpp ON page_links.page = rpp.page but we need to omit using role_id = 3 (or any value) to be able to get that. But I do want to specify the role_id as well and get the results. I need the query to be able to get this result. I would appreciate any replies that could help me with this. If you can suggest me any changes as well to the table(s) design to be able to achieve the desired result, that's good as well. Thanks in advance.
Use:
SELECT pl.page,
r.is_allowed,
r.role_id
FROM PAGE_LINKS pl
LEFT JOIN RPP r ON r.page = pl.page
AND OR r.role_id = 3
Previously:
SELECT pl.page,
r.is_allowed,
r.role_id
FROM PAGE_LINKS pl
LEFT JOIN RPP r ON r.page = pl.page
WHERE r.role_id IS NULL OR r.role_id = 3
This query won't return pages that have a role_id other than 3 and that aren't null.
Try this:
SELECT page_links.page, is_allowed, role_id
FROM page_links
LEFT JOIN rpp
ON rpp.page = page_links.page AND rpp.role_id = 3
SELECT page_links.page, new_rpp.is_allowed, new_rpp.role_id
FROM page_links LEFT JOIN (SELECT * FROM rpp where role_id = 3) as new_rpp USING(page)
This will give you your desired results, it will not only show the pages that correspond to role_id = 3 but will also show all the pages that don't have a corresponding entry in rpp table for role_id = 3
So this should produce the result-set that you desire.