UNNEST function in MYSQL like POSTGRESQL - mysql

Is there a function like "unnest" from POSTGRESQL on MYSQL?
Query (PSQL):
select unnest('{1,2,3,4}'::int[])
Result (as table):
int |
_____|
1 |
_____|
2 |
_____|
3 |
_____|
4 |
_____|

Short answer
Yes, it is possible. From technical viewpoint, you can achieve that with one query. But the thing is - most probably, you are trying to pass some logic from application to data storage. Data storage is intended to store data, not to represent/format it or, even more, apply some logic to it.
Yes, MySQL doesn't have arrays data type, but in most cases it won't be a problem and architecture can be created so it will fit those limitations. And in any case, even if you'll achieve it somehow (like - see below) - you won't be possible to properly work later with that data, since it will be just result set. You may store it, of course - so to, let's say, index later, but then it's again a task for an application - so to create that import.
Also, make sure that it is not a Jaywalker case, so not about storing delimiter-separated values and later trying to extract them.
Long answer
From technical viewpoint, you can do it with Cartesian product of the two row sets. Then use a well known formula:
N = d1x101 + d2x102 + ...
Thus, you'll be able to create a "all-numbers" table and later iterate through it. That iteration, together with MySQL string functions, may lead you to something like this:
SELECT
data
FROM (
SELECT
#next:=LOCATE(#separator,#search, #current+1) AS next,
SUBSTR(SUBSTR(#search, #current, #next-#current), #length+1) AS data,
#next:=IF(#next, #next, NULL) AS marker,
#current:=#next AS current
FROM
(SELECT 0 as i UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) as n1
CROSS JOIN
(SELECT 0 as i UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) as n2
CROSS JOIN
(SELECT
-- set your separator here:
#separator := ',',
-- set your string here:
#data := '1,25,42,71',
-- and do not touch here:
#current := 1,
#search := CONCAT(#separator, #data, #separator),
#length := CHAR_LENGTH(#separator)) AS init
) AS joins
WHERE
marker IS NOT NULL
The corresponding fiddle would be here.
You should also notice: this is not a function. And with functions (I mean, user-defined with CREATE FUNCTION statement) it's impossible to get result row set since function in MySQL can not return result set by definition. However, it's not true to say that it's completely impossible to perform requested transformation with MySQL.
But remember: if you are able to do something, that doesn't mean you should do it.

This sample fetchs all "catchwords" from Table data, wich are seperated by ","
Maximum values in the commaseparated list is 100
WITH RECURSIVE num (n) AS (
SELECT 1
UNION ALL
SELECT n+1 FROM num WHERE n<100 -- change this, if more than 100 elements
)
SELECT DISTINCT substring_index(substring_index(catchwords, ',', n), ',', -1) as value
FROM data
JOIN num
ON char_length(catchwords) - char_length(replace(catchwords, ',', '')) >= n - 1
In newer Version of MySQL/MariaDB you can use JSON_TABLE if you can JOIN the elements:
SELECT cat.catchword, dat.*
FROM data dat
CROSS JOIN json_table(concat('[',dat.catchwords, ']')
, '$[*]' COLUMNS(
catchword VARCHAR(50) PATH '$'
)
) AS words

Related

MySQL group by comma seperated list unique [duplicate]

This question already has answers here:
Is storing a delimited list in a database column really that bad?
(10 answers)
Closed 2 years ago.
The column textfield has comma-seperated list values
ID | textfield
1 | english,russian,german
2 | german,french
3 | english
4 | null
I'm attempting to count the amount of languages in textfield. The default language is "English", so if null then "English". The correct amount of languages is 4(english,russian,german,french).
Here is my query to attempt doing this:
SELECT SUM((length(`textfield`) - length(replace(`textfield`, ',', '')) + 1)) as my
FROM yourtable;
The result i get is 6, i don't know how to group the languages.
Here is fiddle
http://sqlfiddle.com/#!9/0e532/1
The desired result is 4. How do i solve?
Identifying the source of error
What your query is doing is counting how many languages in each row, and adding them all together. Your query does not take into account duplicates. Since English shows up twice in the table, it is counted twice (and German, too), hence in your example six. Also, another issue is that your current code considers null as what null truly means, the absence of a value.
For example, if your database was
ID | textfield
---|----------
1 | null
you would also be arriving at incorrect results (more on this below).
Solution
This gets you a comma separated result of the languages, no duplicates.
SELECT
GROUP_CONCAT(DISTINCT SUBSTRING_INDEX(SUBSTRING_INDEX(textfield, ',', n.digit+1), ',', -1)) textfield
FROM
yourtable
INNER JOIN
(SELECT 0 digit UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6) n
ON LENGTH(REPLACE(textfield, ',', '')) <= LENGTH(textfield)-n.digit;
This query can serve as a subquery for what you were attempting to do in the question prompt. In other words, instead of the length('textfield') ... you would provide the resulting column name from this query
Null as in English
This logic should not be implemented at the database level, IMHO. If you want to go ahead and consider null entries as English, that is fine. The downside is the example I provided for you before. When you have a query that solves for the total languages in the database, if English wasn't an explicitly stated language and instead just a null value, then the query wouldn't 'count' English (it's null). But you can't just add 1 every time you find the amount of languages because English might already be explicit.
Recommendations:
Avoid comma separated lists in databases by normalizing your data
No value makes sense for a null field
For version 5.6 (like in the fiddle)
SELECT COUNT(DISTINCT SUBSTRING_INDEX(SUBSTRING_INDEX(languages.textfield, ',', numbers.num), ',', -1)) languages_count
FROM (SELECT COALESCE(textfield, 'english') textfield
FROM yourtable) languages
JOIN (SELECT 1 num UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) numbers
ON numbers.num <= LENGTH(languages.textfield) - LENGTH(REPLACE(languages.textfield, ',', '')) + 1;
fiddle
For version 8.x (as claimed in a comment)
SELECT COUNT(DISTINCT jsontable.value) languages_count
FROM yourtable
CROSS JOIN JSON_TABLE( CONCAT('["', REPLACE(COALESCE(textfield, 'english'), ',', '","'), '"]'),
"$[*]" COLUMNS( value VARCHAR(254) PATH "$" )
) AS jsontable;
fiddle

convert all JSON columns into new table

I currently have a table structured like:
customer_id name phoneNumbers
1 Adam [{'type':'home','number':'687-5309'} , {'type':'cell','number':'123-4567'}]
2 Bill [{'type':'home','number':'987-6543'}]
With the phoneNumbers column set as a JSON column type.
For simplicity sake though I am wanting to covert all the JSON phone numbers into a new separate table.
Something like:
phone_id customer_id type number
1 1 home 687-5309
2 1 cell 123-4567
3 2 home 987-6543
It seems like it should be do-able with OPENJSON but so far I haven't had any luck in figuring out how to declare it correctly. Any help is appreciated.
USE recursive CTE with 1 and recurse upto json_length.
SELECT c.*, JSON_LENGTH(c.phoneNumbers) as json_length
from customers c;
then use concat to pass that element_id in Extract Query:
(json_unquote(JSON_EXTRACT(phoneNumbers, CONCAT('$.type.',1))), json_unquote(JSON_EXTRACT(phoneNumbers, CONCAT('$.number.',1))))
(json_unquote(JSON_EXTRACT(phoneNumbers, CONCAT('$.type.',2))), json_unquote(JSON_EXTRACT(phoneNumbers, CONCAT('$.number.',1))))
-
-
-
(json_unquote(JSON_EXTRACT(phoneNumbers, CONCAT('$.type.',json_length))), json_unquote(JSON_EXTRACT(phoneNumbers, CONCAT('$.number.',json_length))))
You can do something like this:
SELECT id,
name,
JSON_UNQUOTE(JSON_EXTRACT(phone, CONCAT("$[", seq.i, "]", ".", "number"))) AS NUMBER,
JSON_UNQUOTE(JSON_EXTRACT(phone, CONCAT("$[", seq.i, "]", ".", "type"))) AS TYPE
FROM customer, (SELECT 0 AS I UNION ALL SELECT 1) AS seq
WHERE seq.i < json_length(phone)
The trick is (SELECT 0 as i union all SELECT 1), depends on your JSON array's length you may need to add more index. You can find out the max length by:
SELECT MAX(JSON_LENGTH(phone)) FROM customer;
Please change CTE defination syntax according to MySQL\Maria versions.
WITH RECURSIVE cte_recurse_json AS
(
SELECT customer_id, phone_numbers, 0 as recurse, JSON_LENGTH(c.phoneNumbers) as json_length,
json_unquote(JSON_EXTRACT(phoneNumbers, CONCAT('$[',0,'].type'))) as type,
json_unquote(JSON_EXTRACT(phoneNumbers, CONCAT('$[',0,'].number'))) as number
FROM table
UNION ALL
SELECT t.customer_id, t.phone_numbers, ct.recurse + 1 as recurse, t.json_length,
json_unquote(JSON_EXTRACT(ct.phoneNumbers, CONCAT('$[',ct.recurse,'].type'))) as type,
json_unquote(JSON_EXTRACT(ct.phoneNumbers, CONCAT('$[',ct.recurse,'].number'))) as number
FROM TABLE t
INNER JOIN cte_recurse_json ct ON t.customer_id = ct.customer_id
WHERE ct.recurse < json_length
)
SELECT customer_id, type, number FROM cte_recurse_json;

Math operations on products alpha-num serial numbers | Database Design

So in my database, I got the tables
Product (prodId,Name,Price)
Box (BoxId,prodId,From,To,Available)
'From' represents the first serial number. And 'To' the ending serial.
Calculating 'To' sub 'From' gives the quantity of products.
A client comes and makes an order of a given product with a given quantity. What I need ,is given the 'From' serial number,I calculate 'From' + Quantity.
If the serial numbers were only sequential integers. This would be easy. But this applies to all types of products with different serial numbers.
For ex :
Box( 1,1,ABC00000C,ABC00099K,100)
What I want to achieve is this :
SELECT From + 50 FROM BOX
How Can i deal with the serial number to get the order ending serial ?
To deal with such serial numbers you need either (option 1) a fn(x) calculating a serial number given an integer x, or (option 2) a list of available serial numbers.
Option 1 is easiest to implement, but it requires that the person thinking up the serial number format actually did think of making up a conversion formula to convert an integer into a serial number also. If such a formula exists, all you need to do is determine the integer value for the "from"-value, add 50 to this integer value 'x' and determine the serial number for 'x + 50'.
Option 2 requires that you have a list, or can generate a list of serial numbers, plus those serial numbers must be (somehow) logically ordered. Option 2 then applies one of many ways SQL server provides to get the next 50 rows from this list, starting from the row with value "From" in it. Examples of such methodes are "select top (50) ...", window function "row_number() over (order by ...)", "select ... order by ... offset n rows fetch next 50 rows only" or even a cursor.
Added after comment from Wildfire:
I suggest you create a table holding the serials for option 2. Let me explain this by giving an example: what would you do if one item with serial n + 5 happens to have fallen of the production line and was damaged beyond repair? I.e. this one serial number will never be shipped to a customer. I bet you are not going to ship a box with one less item when this happens, nor are you going to discard 49 undamaged products because the one item is missing. Instead you will probably put all products with serials n to n + 4 and n + 6 to n + 51 in a box, leaving serial n + 5 out. In a perfect world this will of course never happen, but in real life things do go wrong sometimes, so you need to able to cope with -for example- missing serials. So I would really suggest creating a table with all serials available for boxing, and simply have your boxing process read it's next 50 serials from this table.
And option 1 can work, even if the serial itself is non-numerical but can be calculated into a numerical. It's just a little more complicated. That's why I said a formula must exist for the serials for the method to work. Here's an example how you could add 50 to serial "ABC00000C", making "ABC00001Y" the to serial:
declare #from varchar(9) = 'ABC00000C';
declare #from_int bigint;
with cteSerialCharacters as (
-- The set of characters used in a serial.
-- As an example I've taken all number characters plus
-- all capital letters from the alphabet excluding any
-- of these that are easily misread.
select '0123456789ABCDEFGHJKLMNPRSTVWXYZ' as chars
),
cteNumberGenerator as (
select cast (row_number() over (order by (select null)) as bigint) as n
from ( select 1 union all select 1 union all select 1 union all select 1
union all select 1 union all select 1 union all select 1
union all select 1 union all select 1
) t (xyz)
)
select
#from_int = sum(power(s.base, (n - 1)) * (-1 + charindex(substring(reverse(s.serial), n.n, 1), s.characterset)))
from (
select
#from,
cast(len(ch.chars) as bigint),
ch.chars
from cteSerialCharacters ch
) s (serial, base, characterset)
inner join cteNumberGenerator n on (n.n <= len(s.serial));
select #from, #from_int;
declare #to varchar(9);
declare #to_int bigint;
select #to_int = #from_int + 50;
with cteSerialCharacters as (
-- The set of characters used in a serial.
-- As an example I've taken all number characters plus
-- all capital letters from the alphabet excluding any
-- of these that are easily misread.
select '0123456789ABCDEFGHJKLMNPRSTVWXYZ' as chars
),
cteNumberGenerator as (
select cast (row_number() over (order by (select null)) as bigint) as n
from ( select 1 union all select 1 union all select 1 union all select 1
union all select 1 union all select 1 union all select 1
union all select 1 union all select 1
) t (xyz)
)
select
#to = (
select
substring(s.characterset, 1 + (#to_int / power(s.base, n.n - 1)) % s.base, 1) as [text()]
from (
select
cast(len(ch.chars) as bigint),
ch.chars
from cteSerialCharacters ch
) s (base, characterset)
cross join cteNumberGenerator n
order by n.n desc
for xml path(''), type
).value('text()[1]', 'varchar(9)')
select #to, #to_int;

Adding another value below the row

Hey guys i have did some coding in mysql to add a new line value to a row..
SELECT
babe
FROM
(SELECT
concat_ws(' ', 'assword \n') AS babe,
) test;
When i did like this i get an output like
BABE
assword name
What i need is an output like
BABE
assword
name(this would be below assword)
Is there any mysql functions to do this ??...or can i UPDATE the row ??..
I am a newbie in mysql. Hope you guys can help me out..Thanks in advance..
The statement includes a newline character in the babe column. You can confirm this by using the HEX() function to view the character encodings.
For example:
SELECT HEX(t.babe)
FROM ( SELECT CONCAT_WS(' ', 'assword \n') AS babe ) t
On my system, that Will output:
617373776F7264200A
It's easy enough to understand what was returned
a s s w o r d \n
61 73 73 77 6F 72 64 20 0A
(In the original query, there's an extraneous comma that will prevent the statement from running. Perhaps there was another expression in the SELECT list of the inline view, and that was returning the 'name' value that's shown in the example output. But we don't see any reference to that in the outer query.
It's not clear why you need the newline character. If you want to return:
BABE
-----------
asssword
name
That looks like two separate rows to me. But it's valid (but peculiar) to do this:
SELECT t.babe
FROM ( SELECT CONCAT_WS(' ', 'assword \nname') AS babe ) t
FOLLOWUP
Q: i just wanted to know how to add a new row below the assword ..if u know please edit the answer
It's not clear what result you are trying to achieve. The specification, divorced from the context of a use-case, is just bizarre.
A: If I had a need to return two rows: one row with the literal 'assword' and another row "below" it with the literal 'name', I could do this:
( SELECT 'assword' AS some_string )
UNION ALL
( SELECT 'name' AS some_string )
ORDER BY some_string
In this particular case, we can get the ordering we need by a simple reference to the column in the ORDER BY clause.
In the more general case, when there isn't a convenient expression for the ORDER BY clause, I would add an additional column, and perform a SELECT on the resultset from the UNION ALL operation. In this example, that "extra" column is named seq:
SELECT t.some_string
FROM ( SELECT 'assword' AS some_string, 1 AS seq
UNION ALL SELECT 'name', 2
)
ORDER BY t.seq
As another example:
( SELECT 'do' AS tone, 1 AS seq )
UNION ALL ( SELECT 're', 2 )
UNION ALL ( SELECT 'mi', 3 )
UNION ALL ( SELECT 'fa', 4 )
ORDER BY seq
I'd only need to add an outer SELECT if I needed a projection operation (for example, to remove the seq column from the returned resultset.
SELECT t.tone
FROM ( SELECT 'do' AS tone, 1 AS seq
UNION ALL SELECT 're', 2
UNION ALL SELECT 'mi', 3
UNION ALL SELECT 'fa', 4
)
ORDER BY t.seq

SQL, build a query using data provided in the query itself

For experimental purposes only.
I would like to build a query but not querying data extracted for any table but querying data provided in the query it self. Like:
select numbers.* from (1, 2, 3) as numbers;
or
select numbers.* from (field1 = 1, field2 = 2, field3 = 3) as numbers;
so I can do things like
select
numbers.*
from (field1 = 1, field2 = 2, field3 = 3) as numbers
where numbers.field1 > 1;
If the solution is specific for a database engine could be interesting too.
If you wanted the values to be on separate rows instead of three fields of the same row, the method is the same, just one row per value linked with a union all.
select *
from(
select 1 as FieldName union all
select 2 union all
select 3 union all
select 4 union all -- we could continue this for a long time
select 5 -- the end
) as x;
select numbers.*
from(
select 1 ,2, 3
union select 3, 4, 5
union select 6, 7, 8
union select 9, 10, 11 -- we could continue this for a long time
union select 12, 13, 14 -- the end
) as numbers;
This works with MySQL and Postgres (and most others as well).
[Edit] Use union all rather than just union as you do not need to remove duplicates from a list of constants. Give the field(s) in the first select a meaningful name. Otherwise, you can't specify a specific field later on: where x.FieldName = 3.
If you don't provide meaningful names for the fields (as in the second example), the system (at least MySQL where this was tested) will assign the name "1" for the first field, "2" as the second and so on. So, if you want to specify one of the fields, you have to write expressions like this:
where numbers.1 = 3
Use the values row constructor:
select *
from (values (1),(2),(3)) as numbers(nr);
or using a CTE.
with numbers (nr) as (
values (1),(2),(3)
)
select *
from numbers
where nr > 2;
Edit: I just noticed that you also taggeg your question with mysql: the above will not work with MySQL, only with Postgres (and a few other DBMS)
You can use a subquery without table like so:
SELECT
numbers.*
FROM (
SELECT
1 AS a,
2 AS b,
3 AS c
UNION
SELECT
4,
5,
6
) AS numbers
WHERE
numbers.a > 1
If you like queries to always have a table referenced there is a Psuedo table that always has 1 row and no columns called DUAL, you can use it like so:
SELECT
numbers.*
FROM (
SELECT
1 AS a,
2 AS b,
3 AS c
FROM
DUAL
UNION
SELECT
4,
5,
6
FROM
DUAL
) AS numbers
WHERE
numbers.a > 1