¿Cómo se usa la cláusula "WITH" en MySQL?

112

Estoy convirtiendo todas mis consultas de SQL Server a MySQL y todas mis consultas que tienen WITHen ellas están fallando. He aquí un ejemplo:

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
fuente
1
Has simplificado esa consulta, ¿verdad? No hay ninguna razón para usar un CTE allí.
JeremyWeir
2
@NeilMcGuigan ¡Dios mío! Este es uno de los comentarios más divertidos que he visto en este sitio (aunque en realidad no es tan divertido, ¡pero la descaro!;)) +1.
Juan Carlos Coto
Creo que esta es una pregunta relacionada o duplicada Generando una serie de fechas
Adam Porad
2
@NeilMcGuigan La mayoría de los servicios de alojamiento solo ofrecen MySQL o MariaDB, que sin contar el dolor de cabeza del proceso de migración de MySQL a PostgreSQL, es más fácil actualizar a MySQL 8 o MariaDB 10.2.1
Ivanzinho

Respuestas:

135

MySQL anterior a la versión 8.0 no es compatible con la cláusula WITH (CTE en el lenguaje de SQL Server; Factorización de subconsultas en Oracle), por lo que le queda usar:

  • Mesas TEMPORALES
  • Tablas DERIVADAS
  • vistas en línea (efectivamente, lo que representa la cláusula WITH: son intercambiables)

La solicitud de la función se remonta a 2006.

Como se mencionó, proporcionó un ejemplo deficiente: no es necesario realizar una subselección si no está alterando la salida de las columnas de ninguna manera:

  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

He aquí un mejor ejemplo:

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
Ponis dios mio
fuente
24
Esto debería mencionar que CTE generalmente admite la recursividad, lo que no puede hacer con una subconsulta
Hogan
8
Esta pregunta trata sobre "imitar" el soporte de CTE en MySQL - una cosa que no se puede hacer es la funcionalidad recursiva de CTE en todas las plataformas que lo soportan, ese era mi punto.
Hogan
8
Sip. Y todavía no lo han implementado en su rama de lanzamiento. Aparentemente "mostraron" esta "característica" en PHPCONFERENCE2010 en Londres. Este comentario sobre ese informe de error es revelador. [7 de octubre de 2008 19:57] Stuart Friedberg: "Valeriy, ustedes deben tener un retraso increíble. Treinta y tres meses entre la presentación de una solicitud y la obtención de un primer reconocimiento es un período de tiempo asombroso. Gracias por considerar la solicitud. "
Shiva
5
Parece que esto se está agregando a mysql 8 (el enlace sigue siendo bugs.mysql.com/bug.php?id=16244 )
Brian
9
Esta respuesta debe eliminarse: en 2018, MySQL ahora admite la cláusula WITH
jcansell
26

El equipo de desarrolladores de Mysql anunció que la versión 8.0 tendrá expresiones de tabla comunes en MySQL (CTE) . Entonces será posible escribir consultas como esta:


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
fuente
bugs.mysql.com/bug.php?id=16244 (Esto está planeado para 8.0) + (Los CTE recursivos están en MySQL 8.0.1 y más reciente)
gavenkoa
19

En Sql, la declaración with especifica un conjunto de resultados con nombre temporal, conocido como expresión de tabla común (CTE). Puede usarse para consultas recursivas, pero en este caso, se especifica como subconjunto. Si mysql permite subselectes, lo intentaría

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
fuente
Aquí hay una introducción para principiantes a CTE thecodeframework.com/introduction-to-mysql-cte
Gagan
6

Seguí el enlace compartido por lisachenko y encontré otro enlace a este blog: http://guilhembichot.blogspot.co.uk/2013/11/with-recursive-and-mysql.html

La publicación presenta formas de emular los 2 usos de SQL WITH. Muy buena explicación sobre cómo funcionan para hacer una consulta similar a SQL WITH.

1) Use WITH para no tener que realizar la misma subconsulta varias veces

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) Las consultas recursivas se pueden realizar con un procedimiento almacenado que hace que la llamada sea similar a una consulta recursiva.

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,
""
);

Y este es el código o el procedimiento almacenado

# 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
fuente
5

La función 'Expresión de tabla común' no está disponible en MySQL, por lo que debe ir a hacer una vista o tabla temporal para resolver, aquí he usado una tabla temporal.

El procedimiento almacenado mencionado aquí resolverá su necesidad. Si quiero obtener todos los miembros de mi equipo y sus miembros asociados, este procedimiento almacenado ayudará:

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

Código:

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

Esto se puede llamar usando:

mysql>call user_hier ('admin')//
Kanagu
fuente
2

Esa característica se denomina expresión de tabla común http://msdn.microsoft.com/en-us/library/ms190766.aspx

No podrá hacer exactamente lo mismo en mySQL, lo más fácil probablemente sería crear una vista que refleje ese CTE y simplemente seleccionar de la vista. Puede hacerlo con subconsultas, pero funcionará muy mal. Si se encuentra con algún CTE que haga recursividad, no sé cómo podría volver a crearlo sin usar procedimientos almacenados.

EDITAR: Como dije en mi comentario, ese ejemplo que publicó no necesita un CTE, por lo que debe haberlo simplificado para la pregunta, ya que se puede escribir simplemente como

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
fuente
4
@derobert: No es cierto. Una vista tiene metadatos (es decir CREATE/DROP VIEW) y puede otorgar privilegios sobre una vista.
Bill Karwin
1

Me gustó la respuesta de @ Brad de este hilo , pero quería una forma de guardar los resultados para su posterior procesamiento (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 ;

Que produce:

dt        |
----------|
2015-01-01|
2015-01-02|
2015-01-03|
2015-01-04|
2015-01-05|
2015-01-06|
sinecospi
fuente