Select column values as column headers in SQL? - php

I have a table like this:
------------------------------------------------------
ID | Date | ClientName | TransactionAmount |
------------------------------------------------------
1 | 6/16/13 | C1 | 15 |
------------------------------------------------------
2 | 6/16/13 | C1 | 10 |
------------------------------------------------------
3 | 6/16/13 | C2 | 10 |
------------------------------------------------------
4 | 6/17/13 | C2 | 20 |
------------------------------------------------------
And I would like to get something like this:
------------------------------------------------------------------------
Date | C1_Total_Amount_Transacted | C2_Total_Amount_Transacted |
------------------------------------------------------------------------
6/16/13 | 25 | 10 |
------------------------------------------------------------------------
6/17/13 | 0 | 20 |
In the second table Date is unique also I there are x clients in the databse the
resul table will have x + 1 columns (1 fore date and x one for each client).
There might be necessary to write some PHP code and more querys, any working solution
is perfect, I don`t need a full SQL solution.
Thanks

I presume that you are rather new to SQL. This type of query requires conditional summation. And it is quite easy to express in SQL:
select `date`,
sum(case when Client_Name = 'C1' then TransactionAmount else 0 end) as C1,
sum(case when Client_Name = 'C2' then TransactionAmount else 0 end) as C2
from t
group by `date`
But, you have to list each client in the query. You always have to specify the exact column headers for a SQL query. If you don't know them, then you need to create the SQL as a string and then execute it separately. This is a rather cumbersome process.
You can often get around that by using group_concat(). This puts the values in a single column, with a separator of your choice (default is a comma):
select `date`, group_concat(amount)
from (select `date`, ClientName, sum(TransactionAmount) as amount
from t
group by `date`, ClientName
) t
group by `date`

Related

How to GROUP GROUP_CONCAT()?

I am looking for a way to get groups of the GROUP_CONCAT() function in a single query, for example.
My current code
SELECT
SUBSTRING_INDEX(GROUP_CONCAT(service_info.ip_address SEPARATOR ','),',',service_plans.aggregation) AS ip_address
FROM
services
LEFT JOIN
service_info
ON
service_info.service_id = services.id
LEFT JOIN
service_plans
ON
service_plans.id = services.service_plan_id
WHERE
service_plans.id = '2'
I want to group the IP addresses by a specific number(the $group_by variable if you see in the query) but then separate by a different character such as ":" or something.
Essentially I want my output to look like:
If $group_by=2: 10.1.1.2,10.1.1.3:10.1.1.4,10.1.1.5
If $group_by=3: 10.1.1.2,10.1.1.3,10.1.1.4:10.1.1.5
Is this possible to implement into my current query?
UPDATE: table structure
Table service_plans
id | name | aggregation
-----------------------------------------
1 | Uncapped 10Mbps 20:1 | 20
2 | Uncapped 20Mbps 10:1 | 10
3 | Capped 30Mbps | 0
Table services
id | service_plan_id | description
------------------------------------
1 | 2 | Phone
2 | 2 | Laptop
3 | 2 | PC
4 | 2 | TV
5 | 2 | Test
Table service_info
id | service_id | ip_address
------------------------------
1 | 1 | 10.1.1.2
2 | 2 | 10.1.1.3
3 | 3 | 10.1.1.4
4 | 4 | 10.1.1.5
5 | 5 | 10.1.1.6
I am trying to get an array of ip_address's concatenated and separated by a comma but the in groups of however much the service_plans.aggregation value is.
If aggregation is 2, then my output should be:
10.1.1.2,10.1.1.3:10.1.1.4,10.1.1.5
As you can see they are in groups of 2 and then the next group is separated by a colon(:)
If aggregation is 3, then my output should be:
10.1.1.2,10.1.1.3,10.1.1.4:10.1.1.5
As you can see they are in groups of 3 and then the next group is separated by a colon(:) and so on
Your post is a little confusing. What would be helpful is if you posted sample data, and then posted what you want your query to return. I'll give you an answer to what I think you're asking, based on the subject of your post.
ServicePlanIPs
service_plan_id | ip_address
-------------------------------
1 | 192.168.70.1
1 | 192.168.70.2
1 | 192.168.70.3
2 | 192.168.70.4
2 | 192.168.70.5
2 | 192.168.70.6
If you run this query against ServicePlanIPs:
SELECT service_plan_id, GROUP_CONCAT(ip_address) as ip_addresses
FROM ServicePlanIPs
GROUP BY service_plan_id
You will get:
service_plan_id | ip_addresses
-------------------------------
1 | 192.168.70.1, 192.168.70.2, 192.168.70.3
2 | 192.168.70.4, 192.168.70.5, 192.168.70.6
I don't guarantee this will run out of the box, but it should get you on the right track. Hope it helps. Note - if you're using a version of mysql which supports window functions, you can do something similar to the below and use the natively supported RANK function instead of doing it manually with variables.
SET #curRank := 0;
SET #concatIps := '';
SELECT
sp.id,
#curRank := #curRank + 1 AS rank,
IF(MOD(#curRank, (SELECT aggregation FROM service_plans WHERE id = {service_plan_id}) = 0, #concatIps := CONCAT(#concatIps, ':', s.ip_address), #concatIps := CONCAT(#concatIps, ',', s.ip_address))
FROM service_plans sp
JOIN services s
ON sp.id = s.service_plan_id
JOIN service_info si
ON si.service_id = s.id
WHERE sp.id = {service_plan_id}
ORDER BY service_info_id

How to write MySql select statement to get all defined settings or default settings not overridden for an id in one statement

In a table myTable defined as:
+----+---------+-----------+
| id | name | value |
|----+---------+-----------+
| 7 | hand | right |
| 5 | hand | left |
| 0 | hand | both |
| 0 | feet | both |
| 0 | eyes | green |
| 9 | eyes | blue |
| 2 | eyes | white |
| 2 | hand | raised |
+----+---------+-----------+
Default settings are controlled by id = 0.
My question is how to write a select statement to get name,value for id = 5 in one query that will include set for id = 5 and any defaults not overridden.
The results should be:
+---------+-----------+
| name | value |
+---------+-----------+
| hand | left |
| feet | both |
| eyes | green |
+---------+-----------+
It isn't clarified if the ordering of the result set is important, so might as well try:
SELECT name,
value
FROM MyTable
WHERE id = 5
AND id NOT IN
(
SELECT id
FROM MyTable
WHERE id = 0
GROUP BY MyTable.id
)
UNION
SELECT name,
value
FROM MyTable
WHERE id = 0
AND name NOT IN
(
SELECT name
FROM MyTable
WHERE id = 5
GROUP BY MyTable.name
)
Disclaimer: Tested in SQL Server, but not using anything specific to that version of SQL.
seems this one may work for you:
SELECT id,name,value
FROM test
WHERE id = 5
UNION
SELECT id,name,value
FROM test
WHERE id=0
and name not in (select name from test where id=5)
The following does not return three rows in the result set, it only returns a single row with columns hand, feet and eyes, so it may not work for you. But it should return the data that you're looking for given your conditions:
SELECT (CASE
WHEN EXISTS(SELECT 1 FROM myTable WHERE name='hand' AND id=5)
THEN (SELECT `value` FROM myTable WHERE name='hand' AND id=5)
ELSE (SELECT `value` FROM myTable WHERE name='hand' AND id=0)
END) AS hand,
(CASE
WHEN EXISTS(SELECT 1 FROM myTable WHERE name='feet' AND id=5)
THEN (SELECT `value` FROM myTable WHERE name='feet' AND id=5)
ELSE (SELECT `value` FROM myTable WHERE name='feet' AND id=0)
END) AS feet,
(CASE
WHEN EXISTS(SELECT 1 FROM myTable WHERE name='eyes' AND id=5)
THEN (SELECT `value` FROM myTable WHERE name='eyes' AND id=5)
ELSE (SELECT `value` FROM myTable WHERE name='eyes' AND id=0)
END) AS eyes
;
Output:
+---------+-----------+-----------+
| hand | feet | eyes |
+---------+-----------+-----------+
| left | both | green |
+---------+-----------+-----------+
This is approach generates 10 different SELECT statements rather than 1, and the accepted answer is, for most applications, probably a better way to go about it.
SELECT * FROM mytable WHERE ID = 5
UNION
SELECT * FROM mytable WHERE ID = 0 AND name NOT IN (SELECT name FROM MyTable
WHERE id = 5)
Should get you the right answer.
http://sqlfiddle.com/#!9/1f516/14

MySQL, Merge selects in order of one record from each select

I have a table that contains too many records and each bunch of records belong to someone:
---------------------
id | data | username
---------------------
1 | 10 | ali
2 | 11 | ali
3 | 12 | ali
4 | 20 | omid
5 | 21 | omid
6 | 30 | reza
now I want to create a query to result me like this:
1-10-ali
4-20-omid
6-30-reza
2-11-ali
5-21-omid
3-12-ali
Is there anyway to create a query to result me one record per each username and then one from another, and another to the end?
Unfortunately MySQL doesn't have a ranking system so you can use UDV (user defined variables) to rank your records like so.
SELECT id, `data`, name
FROM
( SELECT
id, `data`, name,
#rank := if(#name = name, #rank + 1, 1) as rank,
#name := name
FROM test
CROSS JOIN (SELECT #rank := 1, #name := '') temp
ORDER BY name, `data`
) t
ORDER BY t.rank, t.name, t.data
Sql Fiddle to play with
Output:
+---------------------+
| id | data | name |
+-----+------+--------+
| 1 | 10 | ali |
+---------------------+
| 4 | 20 | omid |
+---------------------+
| 6 | 30 | reza |
+---------------------+
| 2 | 11 | ali |
+---------------------+
| 5 | 21 | omid |
+---------------------+
| 3 | 12 | ali |
+---------------------+
The classic SQL approach is a self join and grouping that lets you determine a row's ranking position by counting the number of rows that come before it. As this is probably slower I doubt I could talk you out of the proprietary method but I mention it to give you an alternative.
select t.id, min(t.`data`), min(t.username)
from test t inner join test t2
on t2.username = t.username and t2.id <= t.id
group by t.id
order by count(*), min(t.username)
Your example would work with
SELECT id, `data`, name
FROM tbl
ORDER BY `data` % 10,
username
`data`;
If data and username do not have the desired pattern, then improve on the example.

MySQL divide rows an show result on new row

I am having difficulties with an MySQL query.
The database looks like this:
Name Unit Int
---------------------
A1 Kilo 20
A1 Price 5
A2 Kilo 15
A2 Price 3
Here is what I'm trying to do:
| Name | Unit | int |
| A1 | Kilo | 20 |
| A1 | Price | 5 |
| A1 | K/P | 4 |
| A2 | Kilo | 15 |
| A2 | Price | 3 |
| A2 | K/P | 5 |
As you can see, I want to insert a new row for every A1 and A2 with a result of Kilo divided by Price.
Is this even possible to do?
If so, could anyone point me in the right direction? I am pretty lost here, and have tried to sweep the internet the last many days, whitout result. I have tried far more ideas than I wish to bore you with.
You could do something like this in your SELECT to calculate it on-the-fly with the latest available data (thus preventing hard-storing something that could later become outdated if the Price/Kilo were to change):
SELECT `kilo`.`name`, (`kilo`.`int` / `price`.`int`) AS 'K/P' FROM `stuff` AS `kilo`
LEFT JOIN `stuff` AS `price` ON (`kilo`.`name` = `price`.`name`)
WHERE `kilo`.`unit`='Kilo' AND `price`.`unit`='Price';
This would return:
+------------+
| name | K/P |
|------------|
| A1 | 4 |
| A2 | 5 |
+------------+
And if you'd really want to insert it into your database you could INSERT ... SELECT the above. Like so:
INSERT INTO `stuff` (`name`, `unit`, `int`)
SELECT `kilo`.`name`, 'K/P', (`kilo`.`int` / `price`.`int`) FROM `stuff` AS `kilo`
LEFT JOIN `stuff` AS `price` ON (`kilo`.`name` = `price`.`name`)
WHERE `kilo`.`unit`='Kilo' AND `price`.`unit`='Price';
See the full SQL Fiddle example here.
You can get all results with
SELECT * FROM (
(SELECT * FROM your_table_name)
UNION
(SELECT r1.Name as Name, 'K/P', r1.Int/r2.Int
FROM your_table_name r1, your_table_name r2
WHERE r1.Name=r2.Name AND r1.Unit='Kilo' AND r2.Unit='Price'
)
) AS derived_table
ORDER BY Name, FIELD(Unit,'Kilo','Price','K/P');

Calculate overlapping durations in MySQL/PHP

This is making my head hurt! :P
I have an assignments table, and I'd like to calculate a member's duration based on their assignments. In its simplified form, this would be relatively straight forward.
-------------------------------------------------------------------------
| id | member_id | unit_id | start_date | end_date |
-------------------------------------------------------------------------
| 1 | 2 | 23 | 2013-01-01 | 2013-02-01 |
-------------------------------------------------------------------------
| 2 | 2 | 25 | 2013-02-01 | 2013-03-01 |
-------------------------------------------------------------------------
| 3 | 2 | 27 | 2013-03-01 | NULL |
-------------------------------------------------------------------------
This would just be a matter of doing a SUM() of the DATEDIFF() on start_date and end_date. The issue is that members have the potential to have concurrent assignments.
-------------------------------------------------------------------------
| id | member_id | unit_id | start_date | end_date |
-------------------------------------------------------------------------
| 1 | 2 | 23 | 2013-01-01 | 2013-02-01 |
-------------------------------------------------------------------------
| 2 | 2 | 25 | 2013-02-01 | 2013-03-01 |
-------------------------------------------------------------------------
| 3 | 2 | 30 | 2013-02-15 | 2013-03-01 |*
-------------------------------------------------------------------------
| 4 | 2 | 27 | 2013-03-01 | NULL |
-------------------------------------------------------------------------
Now I have to somehow realize that #3 occurred during the same time as #2, so I shouldn't add it to the SUM().
Going further, what if the member has gaps in their duration?
-------------------------------------------------------------------------
| id | member_id | unit_id | start_date | end_date |
-------------------------------------------------------------------------
| 1 | 2 | 23 | 2013-01-01 | 2013-02-01 |
-------------------------------------------------------------------------
| 2 | 2 | 25 | 2013-02-01 | 2013-02-05 |*
-------------------------------------------------------------------------
| 3 | 2 | 30 | 2013-02-15 | 2013-03-01 |*
-------------------------------------------------------------------------
| 4 | 2 | 27 | 2013-03-01 | NULL |
-------------------------------------------------------------------------
Also, NULL means "current" so that would be CURDATE().
Any ideas?
Here is the idea. Break each record into two to get a list of dates when assignments start and stop. Then determine how many assignments are active on a given date -- basically adding "1" for each start and "-1" for each end and taking the cumulative sum.
Next, you need to determine when the next date is to get periods before doing the final aggregation.
The first part is handled by this query:
select member_id, thedate,
#sumstart := if(#prevmemberid = memberid, #sumstart + isstart, isstart) as sumstart,
#prevmemberid := memberid
from (select member_id, start_date as thedate, 1 as isstart
from assignments
union all
select member_id, end_date, -1 as isstart
from assignments
order by member_id, thedate
) a cross join
(select #sumstart := 0, #prevmemberid := NULL) const;
The rest then uses more variables:
select member_id,
sum(case when sumstart > 0 then datediff(nextdate, thedate) end) as daysactive
from (select member_id, thedate, sumstart,
if(#prevmemberid = memberid, #nextdate, NULL) as nextdate,
#prevmemberid := memberid,
#nextdate = thedate
from (select member_id, thedate,
#sumstart := if(#prevmemberid = memberid, #sumstart + isstart, isstart) as sumstart,
#prevmemberid := memberid
from (select member_id, start_date as thedate, 1 as isstart
from assignments
union all
select member_id, coalesce(end_date, CURDATE()), -1 as isstart
from assignments
order by member_id, thedate
) a cross join
(select #sumstart := 0, #prevmemberid := NULL) const;
) a cross join
(select #nextmemberid := NULL, #nextdate := NULL) const
order by member_id, thedate desc;
) a
group by member_id;
I don't like using variables in this way, because MySQL does not guarantee the ordering of variable assignments in a given select. In practice, though, they are evaluated in the order written (which this query depends on). Although this could be written without variables, without the with statement, window functions, or even views that take subqueries in the from clause, the resulting SQL would be much uglier.
I think it's easier to perform filter out the overlapping assignments in the code rather than in SQL.
You can retrieve all the assignments for a certain member_id, ordered by start_date:
select * from assignments where member_id='2' order by start_date asc
You can then loop over these assignments and filter out the overlapping assignments.
Two assignments A and B are non-overlapping if A ends before B starts or if B ends before A starts.
Because we ordered the results according to start date, we can safely ignore the second case: B will never start before A, so it cannot end before A starts.
We then get something like:
for i=0..assignments.length
for j=i+1..assignments.length
if (assignments[j].start_date < assignments[i].end_date)
assignments[j] = null; // it overlaps -> get rid of it
Then loop over the assignments and sum the durations for the non-null assignments. This should be easy

Categories