Comment utilisez-vous la clause «WITH» dans MySQL?

112

Je convertis toutes mes requêtes SQL Server en MySQL et mes requêtes qui les contiennent WITHéchouent toutes. Voici un exemple:

WITH t1 AS
(
     SELECT article.*, userinfo.*, category.*
     FROM question
     INNER JOIN userinfo ON userinfo.user_userid = article.article_ownerid
     INNER JOIN category ON article.article_categoryid = category.catid
     WHERE article.article_isdeleted = 0
)
SELECT t1.*
FROM t1
ORDER BY t1.article_date DESC
LIMIT 1, 3
user161433
la source
1
Vous avez simplifié cette requête, n'est-ce pas? Il n'y a aucune raison d'utiliser un CTE là-bas.
JeremyWeir
2
@NeilMcGuigan Oh mon Dieu! C'est l'un des commentaires les plus drôles que j'ai vu sur ce site (même si ce n'est vraiment pas si drôle, mais l'effronterie!;)) +1.
Juan Carlos Coto
Je pense que c'est une question liée ou en double Génération d'une série de dates
Adam Porad
2
@NeilMcGuigan La plupart des services d'hébergement n'offrent que MySQL ou MariaDB, qui sans compter les maux de tête du processus de migration de MySQL vers PostgreSQL, il est plus facile de passer à MySQL 8 ou MariaDB 10.2.1
Ivanzinho

Réponses:

135

MySQL antérieur à la version 8.0 ne prend pas en charge la clause WITH (CTE dans le langage SQL Server; Factorisation de sous-requêtes dans Oracle), vous devez donc utiliser:

  • Tables TEMPORAIRES
  • Tables DÉRIVÉES
  • vues en ligne (en fait ce que représente la clause WITH - elles sont interchangeables)

La demande pour la fonctionnalité remonte à 2006.

Comme mentionné, vous avez fourni un mauvais exemple - il n'est pas nécessaire d'effectuer une sous-sélection si vous ne modifiez en aucune façon la sortie des colonnes:

  SELECT * 
    FROM ARTICLE t
    JOIN USERINFO ui ON ui.user_userid = t.article_ownerid
    JOIN CATEGORY c ON c.catid =  t.article_categoryid
   WHERE t.published_ind = 0
ORDER BY t.article_date DESC 
   LIMIT 1, 3

Voici un meilleur exemple:

SELECT t.name,
       t.num
  FROM TABLE t
  JOIN (SELECT c.id
               COUNT(*) 'num'
          FROM TABLE c
         WHERE c.column = 'a'
      GROUP BY c.id) ta ON ta.id = t.id
Poneys OMG
la source
24
Cela devrait mentionner que CTE prend généralement en charge la récursivité - ce que vous ne pouvez pas faire avec une sous-requête
Hogan
8
Cette question concerne "imiter" le support CTE dans MySQL - une chose qui ne peut pas être faite est la fonctionnalité récursive des CTE sur toutes les plates-formes qui le supportent, c'était mon point.
Hogan
8
Ouaip. Et ils ne l' ont toujours pas implémenté dans leur branche de publication. Apparemment, ils ont "montré" cette "fonctionnalité" à PHPCONFERENCE2010 à Londres. Ce commentaire sur ce rapport de bogue est révélateur. [7 oct. 2008 19:57] Stuart Friedberg: "Valeriy, vous devez avoir un arriéré incroyable. Trente-trois mois entre le dépôt d'une demande et l'obtention d'un premier accusé de réception est une durée époustouflante. Merci d'avoir examiné la demande. "
Shiva
5
On dirait que cela est ajouté à mysql 8 (le lien est toujours bugs.mysql.com/bug.php?id=16244 )
Brian
9
Cette réponse doit être supprimée - en 2018, MySQL supporte désormais la clause WITH
jcansell
26

L'équipe de développeurs Mysql a annoncé que la version 8.0 aura des expressions de table communes dans MySQL (CTE) . Il sera donc possible d'écrire des requêtes comme celle-ci:


WITH RECURSIVE my_cte AS
(
  SELECT 1 AS n
  UNION ALL
  SELECT 1+n FROM my_cte WHERE n<10
)
SELECT * FROM my_cte;
+------+
| n    |
+------+
|    1 |
|    2 |
|    3 |
|    4 |
|    5 |
|    6 |
|    7 |
|    8 |
|    9 |
|   10 |
+------+
10 rows in set (0,00 sec)

Lisachenko
la source
bugs.mysql.com/bug.php?id=16244 (Ceci est prévu pour 8.0) + (Les CTE récursifs sont dans MySQL 8.0.1 et plus récent)
gavenkoa
19

Dans SQL, l'instruction with spécifie un jeu de résultats nommé temporaire, appelé expression de table commune (CTE). Il peut être utilisé pour les requêtes récursives, mais dans ce cas, il spécifie comme sous-ensemble. Si mysql autorise les sous-sélections, j'essaierais

select t1.* 
from  (
            SELECT  article.*, 
                    userinfo.*, 
                    category.* 
            FROM    question INNER JOIN 
                    userinfo ON userinfo.user_userid=article.article_ownerid INNER JOIN category ON article.article_categoryid=category.catid
            WHERE   article.article_isdeleted = 0
     ) t1
ORDER BY t1.article_date DESC Limit 1, 3
Adriaan Stander
la source
Voici une introduction pour débutants au CTE thecodeframework.com/introduction-to-mysql-cte
Gagan
6

J'ai suivi le lien partagé par lisachenko et j'ai trouvé un autre lien vers ce blog: http://guilhembichot.blogspot.co.uk/2013/11/with-recursive-and-mysql.html

Le post présente des moyens d'émuler les 2 utilisations de SQL WITH. Vraiment bonne explication sur la façon dont ceux-ci fonctionnent pour faire une requête similaire à SQL WITH.

1) Utilisez WITH pour ne pas avoir à effectuer la même sous-requête plusieurs fois

CREATE VIEW D AS (SELECT YEAR, SUM(SALES) AS S FROM T1 GROUP BY YEAR);
SELECT D1.YEAR, (CASE WHEN D1.S>D2.S THEN 'INCREASE' ELSE 'DECREASE' END) AS TREND
FROM
 D AS D1,
 D AS D2
WHERE D1.YEAR = D2.YEAR-1;
DROP VIEW D;

2) Les requêtes récursives peuvent être effectuées avec une procédure stockée qui rend l'appel similaire à une requête récursive.

CALL WITH_EMULATOR(
"EMPLOYEES_EXTENDED",
"
  SELECT ID, NAME, MANAGER_ID, 0 AS REPORTS
  FROM EMPLOYEES
  WHERE ID NOT IN (SELECT MANAGER_ID FROM EMPLOYEES WHERE MANAGER_ID IS NOT NULL)
",
"
  SELECT M.ID, M.NAME, M.MANAGER_ID, SUM(1+E.REPORTS) AS REPORTS
  FROM EMPLOYEES M JOIN EMPLOYEES_EXTENDED E ON M.ID=E.MANAGER_ID
  GROUP BY M.ID, M.NAME, M.MANAGER_ID
",
"SELECT * FROM EMPLOYEES_EXTENDED",
0,
""
);

Et c'est le code ou la procédure stockée

# Usage: the standard syntax:
#   WITH RECURSIVE recursive_table AS
#    (initial_SELECT
#     UNION ALL
#     recursive_SELECT)
#   final_SELECT;
# should be translated by you to 
# CALL WITH_EMULATOR(recursive_table, initial_SELECT, recursive_SELECT,
#                    final_SELECT, 0, "").

# ALGORITHM:
# 1) we have an initial table T0 (actual name is an argument
# "recursive_table"), we fill it with result of initial_SELECT.
# 2) We have a union table U, initially empty.
# 3) Loop:
#   add rows of T0 to U,
#   run recursive_SELECT based on T0 and put result into table T1,
#   if T1 is empty
#      then leave loop,
#      else swap T0 and T1 (renaming) and empty T1
# 4) Drop T0, T1
# 5) Rename U to T0
# 6) run final select, send relult to client

# This is for *one* recursive table.
# It would be possible to write a SP creating multiple recursive tables.

delimiter |

CREATE PROCEDURE WITH_EMULATOR(
recursive_table varchar(100), # name of recursive table
initial_SELECT varchar(65530), # seed a.k.a. anchor
recursive_SELECT varchar(65530), # recursive member
final_SELECT varchar(65530), # final SELECT on UNION result
max_recursion int unsigned, # safety against infinite loop, use 0 for default
create_table_options varchar(65530) # you can add CREATE-TABLE-time options
# to your recursive_table, to speed up initial/recursive/final SELECTs; example:
# "(KEY(some_column)) ENGINE=MEMORY"
)

BEGIN
  declare new_rows int unsigned;
  declare show_progress int default 0; # set to 1 to trace/debug execution
  declare recursive_table_next varchar(120);
  declare recursive_table_union varchar(120);
  declare recursive_table_tmp varchar(120);
  set recursive_table_next  = concat(recursive_table, "_next");
  set recursive_table_union = concat(recursive_table, "_union");
  set recursive_table_tmp   = concat(recursive_table, "_tmp"); 
  # Cleanup any previous failed runs
  SET @str =
    CONCAT("DROP TEMPORARY TABLE IF EXISTS ", recursive_table, ",",
    recursive_table_next, ",", recursive_table_union,
    ",", recursive_table_tmp);
  PREPARE stmt FROM @str;
  EXECUTE stmt; 
 # If you need to reference recursive_table more than
  # once in recursive_SELECT, remove the TEMPORARY word.
  SET @str = # create and fill T0
    CONCAT("CREATE TEMPORARY TABLE ", recursive_table, " ",
    create_table_options, " AS ", initial_SELECT);
  PREPARE stmt FROM @str;
  EXECUTE stmt;
  SET @str = # create U
    CONCAT("CREATE TEMPORARY TABLE ", recursive_table_union, " LIKE ", recursive_table);
  PREPARE stmt FROM @str;
  EXECUTE stmt;
  SET @str = # create T1
    CONCAT("CREATE TEMPORARY TABLE ", recursive_table_next, " LIKE ", recursive_table);
  PREPARE stmt FROM @str;
  EXECUTE stmt;
  if max_recursion = 0 then
    set max_recursion = 100; # a default to protect the innocent
  end if;
  recursion: repeat
    # add T0 to U (this is always UNION ALL)
    SET @str =
      CONCAT("INSERT INTO ", recursive_table_union, " SELECT * FROM ", recursive_table);
    PREPARE stmt FROM @str;
    EXECUTE stmt;
    # we are done if max depth reached
    set max_recursion = max_recursion - 1;
    if not max_recursion then
      if show_progress then
        select concat("max recursion exceeded");
      end if;
      leave recursion;
    end if;
    # fill T1 by applying the recursive SELECT on T0
    SET @str =
      CONCAT("INSERT INTO ", recursive_table_next, " ", recursive_SELECT);
    PREPARE stmt FROM @str;
    EXECUTE stmt;
    # we are done if no rows in T1
    select row_count() into new_rows;
    if show_progress then
      select concat(new_rows, " new rows found");
    end if;
    if not new_rows then
      leave recursion;
    end if;
    # Prepare next iteration:
    # T1 becomes T0, to be the source of next run of recursive_SELECT,
    # T0 is recycled to be T1.
    SET @str =
      CONCAT("ALTER TABLE ", recursive_table, " RENAME ", recursive_table_tmp);
    PREPARE stmt FROM @str;
    EXECUTE stmt;
    # we use ALTER TABLE RENAME because RENAME TABLE does not support temp tables
    SET @str =
      CONCAT("ALTER TABLE ", recursive_table_next, " RENAME ", recursive_table);
    PREPARE stmt FROM @str;
    EXECUTE stmt;
    SET @str =
      CONCAT("ALTER TABLE ", recursive_table_tmp, " RENAME ", recursive_table_next);
    PREPARE stmt FROM @str;
    EXECUTE stmt;
    # empty T1
    SET @str =
      CONCAT("TRUNCATE TABLE ", recursive_table_next);
    PREPARE stmt FROM @str;
    EXECUTE stmt;
  until 0 end repeat;
  # eliminate T0 and T1
  SET @str =
    CONCAT("DROP TEMPORARY TABLE ", recursive_table_next, ", ", recursive_table);
  PREPARE stmt FROM @str;
  EXECUTE stmt;
  # Final (output) SELECT uses recursive_table name
  SET @str =
    CONCAT("ALTER TABLE ", recursive_table_union, " RENAME ", recursive_table);
  PREPARE stmt FROM @str;
  EXECUTE stmt;
  # Run final SELECT on UNION
  SET @str = final_SELECT;
  PREPARE stmt FROM @str;
  EXECUTE stmt;
  # No temporary tables may survive:
  SET @str =
    CONCAT("DROP TEMPORARY TABLE ", recursive_table);
  PREPARE stmt FROM @str;
  EXECUTE stmt;
  # We are done :-)
END|

delimiter ;
sbaer
la source
5

La fonction 'Common Table Expression' n'est pas disponible dans MySQL, il faut donc aller faire une vue ou une table temporaire à résoudre, ici j'ai utilisé une table temporaire.

La procédure stockée mentionnée ici résoudra votre besoin. Si je veux obtenir tous les membres de mon équipe et leurs membres associés, cette procédure stockée aidera:

----------------------------------
user_id   |   team_id
----------------------------------
admin     |   NULL
ramu      |   admin
suresh    |   admin
kumar     |   ramu
mahesh    |   ramu
randiv    |   suresh
-----------------------------------

Code:

DROP PROCEDURE `user_hier`//
CREATE DEFINER=`root`@`localhost` PROCEDURE `user_hier`(in team_id varchar(50))
BEGIN
declare count int;
declare tmp_team_id varchar(50);
CREATE TEMPORARY TABLE res_hier(user_id varchar(50),team_id varchar(50))engine=memory;
CREATE TEMPORARY TABLE tmp_hier(user_id varchar(50),team_id varchar(50))engine=memory;
set tmp_team_id = team_id;
SELECT COUNT(*) INTO count FROM user_table WHERE user_table.team_id=tmp_team_id;
WHILE count>0 DO
insert into res_hier select user_table.user_id,user_table.team_id from user_table where user_table.team_id=tmp_team_id;
insert into tmp_hier select user_table.user_id,user_table.team_id from user_table where user_table.team_id=tmp_team_id;
select user_id into tmp_team_id from tmp_hier limit 0,1;
select count(*) into count from tmp_hier;
delete from tmp_hier where user_id=tmp_team_id;
end while;
select * from res_hier;
drop temporary table if exists res_hier;
drop temporary table if exists tmp_hier;
end

Cela peut être appelé en utilisant:

mysql>call user_hier ('admin')//
Kanagu
la source
2

Cette fonctionnalité est appelée une expression de table commune http://msdn.microsoft.com/en-us/library/ms190766.aspx

Vous ne pourrez pas faire exactement la chose dans mySQL, la chose la plus simple serait probablement de créer une vue qui reflète cette CTE et de simplement la sélectionner dans la vue. Vous pouvez le faire avec des sous-requêtes, mais cela fonctionnera très mal. Si vous rencontrez des CTE qui font de la récursivité, je ne sais pas comment vous pourriez recréer cela sans utiliser de procédures stockées.

EDIT: Comme je l'ai dit dans mon commentaire, cet exemple que vous avez posté n'a pas besoin d'un CTE, vous devez donc l'avoir simplifié pour la question car il peut être simplement écrit comme

SELECT article.*, userinfo.*, category.* FROM question
     INNER JOIN userinfo ON userinfo.user_userid=article.article_ownerid
     INNER JOIN category ON article.article_categoryid=category.catid
     WHERE article.article_isdeleted = 0
 ORDER BY article_date DESC Limit 1, 3
JeremyWeir
la source
4
@derobert: Ce n'est pas vrai. Une vue a des métadonnées (c'est-à-dire CREATE/DROP VIEW) et vous pouvez accorder des privilèges sur une vue.
Bill Karwin
1

J'ai aimé la réponse de @ Brad dans ce fil , mais je voulais un moyen d'enregistrer les résultats pour un traitement ultérieur (MySql 8):

-- May need to adjust the recursion depth first
SET @@cte_max_recursion_depth = 10000 ; -- permit deeper recursion

-- Some boundaries 
set @startDate = '2015-01-01'
    , @endDate = '2020-12-31' ; 

-- Save it to a table for later use
drop table if exists tmpDates ;
create temporary table tmpDates as      -- this has to go _before_ the "with", Duh-oh! 
    WITH RECURSIVE t as (
        select @startDate as dt
      UNION
        SELECT DATE_ADD(t.dt, INTERVAL 1 DAY) FROM t WHERE DATE_ADD(t.dt, INTERVAL 1 DAY) <= @endDate
    )
    select * FROM t     -- need this to get the "with"'s results as a "result set", into the "create"
;

-- Exists?
select * from tmpDates ;

Ce qui produit:

dt        |
----------|
2015-01-01|
2015-01-02|
2015-01-03|
2015-01-04|
2015-01-05|
2015-01-06|
sinecospi
la source