1

i'm trying to find a better solution, because the actual query takes too long.

lets assume you have 3 tables: tasks, adresses, persons now we have a string like "tobias london" - we will split the string by spaces and get ["tobias","london"] (of course we could have a more complex string with more words)

now we want all tasks, that has matches in adresses OR persons AND ALL words must match

actual query example:

SELECT
  tasks.task_id
FROM
  (
    tasks,
    (SELECT
      adress_id AS id,
      'adress_id' AS typ
    FROM
      adresses
    WHERE adresses.city LIKE 'tobias%'
      OR adresses.street LIKE 'tobias%'
    UNION
    SELECT
      person_id AS id,
      'person_id' AS typ
    FROM
      persons
    WHERE persons.name LIKE 'tobias%') f0,
    (SELECT
      adress_id AS id,
      'adress_id' AS typ
    FROM
      adresses
    WHERE adresses.city LIKE 'london%'
      OR adresses.street LIKE 'london%'
    UNION
    SELECT
      person_id AS id,
      'person_id' AS typ
    FROM
      persons
    WHERE persons.name LIKE 'london%') f1
  )
WHERE (
    f0.typ = 'adress_id'
    AND tasks.adress_id = f0.id
    OR f0.typ = 'person_id'
    AND tasks.person_id = f0.id
  )
  AND (
    f1.typ = 'adress_id'
    AND tasks.adress_id = f1.id
    OR f1.typ = 'person_id'
    AND tasks.person_id = f1.id
  )

this is extremely slow, because of the OR within the WHERE-clauses (and this is just an example with 2 words)

anybody know how to make it better than this?

EDIT: as requested a small db-fiddle:

CREATE TABLE `adresses` (
  `adress_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `street` varchar(200) NOT NULL,
  `house_number` varchar(6) NOT NULL,
  `postal_code` smallint(5) unsigned NOT NULL,
  `city` varchar(100) NOT NULL,
  PRIMARY KEY (`adress_id`),
  KEY `street` (`street`),
  KEY `postal_code` (`postal_code`),
  KEY `city` (`city`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;

/*Data for the table `adresses` */

insert  into `adresses`(`adress_id`,`street`,`house_number`,`postal_code`,`city`) values 
(1,'first','1',12345,'London'),
(2,'second','2',23456,'Paris'),
(3,'third','3',34567,'Okawa'),
(4,'fourth','4',45678,'Berlin'),
(5,'fiveth','5',56789,'ABCDE'),
(6,'sixth','6',65535,'DFGHJH');

/*Table structure for table `persons` */

CREATE TABLE `persons` (
  `person_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL,
  PRIMARY KEY (`person_id`),
  KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;

/*Data for the table `persons` */

insert  into `persons`(`person_id`,`name`) values 
(1,'Anna'),
(2,'John'),
(3,'Michel'),
(4,'Olivia'),
(5,'Pavel'),
(6,'Peter'),
(7,'Sarah'),
(8,'Tobias');

/*Table structure for table `tasks` */

CREATE TABLE `tasks` (
  `task_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `person_id` int(10) unsigned NOT NULL,
  `adress_id` int(10) unsigned NOT NULL,
  `description` varchar(200) NOT NULL,
  PRIMARY KEY (`task_id`),
  KEY `person_id` (`person_id`),
  KEY `adress_id` (`adress_id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;

/*Data for the table `tasks` */

insert  into `tasks`(`task_id`,`person_id`,`adress_id`,`description`) values 
(1,1,2,''),
(2,2,3,''),
(3,3,4,''),
(4,5,6,''),
(5,4,1,''),
(6,1,4,''),
(7,3,3,''),
(8,8,1,'');


SELECT
  tasks.task_id
FROM
  (
    tasks,
    (SELECT
      adress_id AS id,
      'adress_id' AS typ
    FROM
      adresses
    WHERE adresses.city LIKE 'tobias%'
      OR adresses.street LIKE 'tobias%'
    UNION
    SELECT
      person_id AS id,
      'person_id' AS typ
    FROM
      persons
    WHERE persons.name LIKE 'tobias%') f0,
    (SELECT
      adress_id AS id,
      'adress_id' AS typ
    FROM
      adresses
    WHERE adresses.city LIKE 'london%'
      OR adresses.street LIKE 'london%'
    UNION
    SELECT
      person_id AS id,
      'person_id' AS typ
    FROM
      persons
    WHERE persons.name LIKE 'london%') f1
  )
WHERE (
    f0.typ = 'adress_id'
    AND tasks.adress_id = f0.id
    OR f0.typ = 'person_id'
    AND tasks.person_id = f0.id
  )
  AND (
    f1.typ = 'adress_id'
    AND tasks.adress_id = f1.id
    OR f1.typ = 'person_id'
    AND tasks.person_id = f1.id
  )
✓

✓

✓

✓

✓

✓

| task_id |
| ------: |
|       8 |

db<>fiddle here

mech
  • 617
  • 6
  • 16
  • Have you tried [Full-Text Search](https://dev.mysql.com/doc/refman/8.0/en/fulltext-search.html) – Ergest Basha May 20 '22 at 12:22
  • you tink of: WHERE tasks.person_id=persons.person_id AND tasks.adress_id=adresses.adress_id AND MATCH (persons.name, adresses.street, adresses.city) AGAINST ('tobias' IN NATURAL LANGUAGE MODE) AND ... (2nd match-condition)? tried it, but seems incorrect (Incorrect arguments to MATCH) i also don't know if the restrictions would be ok, for example if the search string would be "tobias london 123" i would also check for adresses.hous_number – mech May 20 '22 at 12:42
  • just read https://stackoverflow.com/questions/2610812/mysql-full-text-search-why-am-i-getting-incorrect-arguments-to-match that said all cols from a fulltext search must be of the same table, so it is not an option – mech May 20 '22 at 12:44
  • you still have to use union – Ergest Basha May 20 '22 at 12:46
  • the problem is not searching in persons or adresses, that is relativly fast the problem is the join of 2 or more tables to 2 or more columns of the main-table – mech May 20 '22 at 12:46
  • [Bad Habits to Kick : Using old-style JOINs](https://sqlblog.org/2009/10/08/bad-habits-to-kick-using-old-style-joins). After that you might create a [DBFIDDLE](https://dbfiddle.uk/?rdbms=mysql_8.0), just about 10 records is enough (I will add some more records, before looking into optimizing the query ) – Luuk May 21 '22 at 20:05
  • @Luuk added the db-fiddle, hope it helps – mech May 27 '22 at 07:31

1 Answers1

0

Redesign the schema. The use of typ to pick between two table is a very bad idea in a Relational Database. (As you are finding out.)

After that, it may be useful to have a FULLTEXT index on the columns that could have "tobias%" or "london%". But the problem then comes because the address and person name are in different tables.

If you had both in the same table:

SELECT ...
    WHERE MATCH(firstname, last name, address, city)
          AGAINST("+tobias* +london*" IN BOOLEAN MODE)

And have this index on the table

FULLTEXT(firstname, lastname, address, city)

(Be aware of limitations of word length, etc.)

Another approach

Consider making a separate table (Search) with perhaps two columns:

  • words - a mashup of all the things that users might search on (names, city, etc)
  • task_id (or whatever is the necessary key) -- for joining to the other table(s)

Then use it (for example) thus:

SELECT ...
    FROM Search
    JOIN ... ON Search.task_id = ...
    WHERE MATCH(Search.text) AGAINST ("+word1 +word2" IN BOOLEAN MODE)
    ...

It helps to eliminate any 'short' words when building the query.

This technique takes extra effort when inserting rows. And there is redundant data in the database. (This is a no-no, but is often necessary for performance.)

Rick James
  • 135,179
  • 13
  • 127
  • 222
  • any idea about the redesign? since persons are persons and adresses are adresses ... they have to be in separate tables i think? – mech May 23 '22 at 11:03
  • @mech - I added to my answer. – Rick James May 23 '22 at 14:59
  • tahnk you, but since this is just a basic example (there are way more tables that could be involved) i can't put all together in one table (there are tasks with persons, adresses, services, materials, billing-informations, messages, and so on) so it seems there is no "good" way – mech May 27 '22 at 07:08
  • @mech - I added "Another Approach". – Rick James May 27 '22 at 18:05