Question

I have have a column name MyRoles that contains a list of items (integers) stored in a field named UserRoles. I want to write a query that checks to see if a specific item is in the list. The list will look like this: "1,2,3"

I can't use WHERE MyRoles

What should the query look like?

This is similar to what I am thinking:

SELECT *
FROM MyTable
WHERE MyRoles CONTAINS ('1')

The fact that no answer was easy to implement and would take me farther down an ugly road really makes clear that a normalized database is always the best bet.

Was it helpful?

Solution

Convert it to an array:

SELECT *
FROM MyTable
WHERE ('{' || MyRoles || '}')::int[] && array[1]

Better yet, you can use an index on the above mess. Casting text to an array type outright will get rejected when building an array, but you can work around it:

create function role_scan(text) returns int[] as $$
  select ('{' || $1 || '}')::int[];
$$ language sql immutable strict;

create index on MyTable using gin (role_scan(MyRoles));

-- actually use the index
SELECT *
FROM MyTable
WHERE role_scan(MyRoles) && array[1]

There is a caveat in adding the index, that you should be aware of. The stats collector doesn't look (up to 9.1 anyway) into the actual array values. The selectivity of the overlap operator (1/200, i.e. very selective) is hard-coded for all intents and purposes. So if you're querying against very common values, you might get an index scan where inappropriate. One workaround is to call the underlying overlap method directly (which gives a selectivity of 1/3 and no potential index scan) when you know plenty of roles apply:

SELECT *
FROM MyTable
WHERE arrayoverlap(role_scan(MyRoles), array[1])

OTHER TIPS

You can use LIKE:

SELECT *
FROM MyTable
WHERE MyRoles LIKE ('%1%')

This is likely to perform very badly (as an index will be fairly useless for such a search). And of course, will also match on 10 even if 1 doesn't exist in the query. You can extend the approach:

SELECT *
FROM MyTable
WHERE MyRoles = '1'
  OR MyRoles LIKE '1,%'
  OR MyRoles LIKE '%,1,%'

A better solution would be to normalize your database and not have multi-valued fields. Use a many-to-many table with single role ids and item ids per row. This is much easier to query.

Some databases will have better facilities for such a query, but these would be extensions and not standard SQL - you didn't mention a specific RDBMS.

Watch out if you use LIKE:

If MyRoles is 2,11 then it will match LIKE('%1%') although you don't want it to.

A painful workaround is to use

SELECT *
FROM MyTable
WHERE MyRoles LIKE ('%,1,%')

but then you need to put leading and trailing commas in every MyRoles entry.

These various ugly facts are the reason everyone is telling you to change your database design and create a "Roles" table.

SELECT *
FROM MyTable
WHERE FIND_IN_SET(1, MyRoles)

EDIT: It works in mysql db server.

EDIT:

find_in_set function for postgres:

create function find_in_set(n int, s text) returns bigint as
$$
select z.row_number
from
(
    select row_number() over(), y.x
    from (select unnest(('{' || $2 || '}')::int[]) as x) as y
) as z
where z.x = $1
$$ language sql;

maybe regular expressions can help here:

SELECT *
FROM MyTable
WHERE MyRoles ~ ('^(.*,)*' || 1 || '(,.*)*$')
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top