如果总列总和小于X,则更新行

时间:2016-08-16 21:57:20

标签: mysql sql database join rdbms

如果总大小小于x,我正试图弄清楚如何更新表格中的行。

这是我的设置:

create table test_limit (
       id int not null auto_increment primary key,
       folder varchar(255),
       status varchar(32) DEFAULT 'awaiting',
       size bigint unsigned default 0,
       request_id varchar(32)
)
ENGINE=InnoDB;

insert into test_limit
  (folder, status, size)
values
  ('/tmp/AAA/bar', 'awaiting', 200 ),
  ('/tmp/AAA/bar', 'awaiting', 200 ),
  ('/tmp/AAA/bar', 'awaiting', 200 ),
  ('/tmp/BBB/bar', 'awaiting', 200 ),
  ('/tmp/BBB/bar', 'awaiting', 200 );

我有一个包含5行的表,每行的大小我要做的就是更新一组行:

  • 具有相同的folder
  • 状态不在in_progresscreated
  • 总限额为400

我想出了以下更新命令:

SET @request_id='bbb';
UPDATE test_limit t1
  JOIN
     ( SELECT folder FROM test_limit WHERE status = 'awaiting' GROUP BY folder limit 1) t2
    ON t1.folder = t2.folder
  LEFT JOIN
     ( SELECT folder FROM test_limit WHERE status IN ('in_progress', 'created') GROUP BY folder limit 1) t3
    ON t1.folder = t3.folder
  JOIN
     ( SELECT id, @total := @total + size AS total  FROM (test_limit, (select @total := 0) t)  WHERE @total < 400 and status='awaiting') t4
    ON t1.id=t4.id
  SET t1.status = 'in_progress',
      t1.request_id = @request_id
  WHERE t1.status = 'awaiting' AND t3.folder is NULL;

但问题是它是第一次工作,但在其他任何时候都不起作用:

mysql> select * from test_limit;
+----+--------------+-------------+------+------------+
| id | folder       | status      | size | request_id |
+----+--------------+-------------+------+------------+
|  1 | /tmp/AAA/bar | in_progress |  200 | bbb        |
|  2 | /tmp/AAA/bar | in_progress |  200 | bbb        |
|  3 | /tmp/AAA/bar | awaiting    |  200 | NULL       |
|  4 | /tmp/BBB/bar | awaiting    |  200 | NULL       |
|  5 | /tmp/BBB/bar | awaiting    |  200 | NULL       |
+----+--------------+-------------+------+------------+
5 rows in set (0.07 sec)

更新

上述结果对于第一次运行是正确的。我想在第二次运行中实现的目标(比如request_id ='aaa'):

mysql> select * from test_limit;
+----+--------------+-------------+------+------------+
| id | folder       | status      | size | request_id |
+----+--------------+-------------+------+------------+
|  1 | /tmp/AAA/bar | in_progress |  200 | bbb        |
|  2 | /tmp/AAA/bar | in_progress |  200 | bbb        |
|  3 | /tmp/AAA/bar | awaiting    |  200 | NULL       |
|  4 | /tmp/BBB/bar | in_progress |  200 | aaa        |
|  5 | /tmp/BBB/bar | in_progress |  200 | aaa        |
+----+--------------+-------------+------+------------+
5 rows in set (0.07 sec)

在第三次运行中它不应该更新任何东西,因为所有的值都是“in_progress”。

我怎样才能做到这一点?

2 个答案:

答案 0 :(得分:1)

这是使用存储过程的解决方案。虽然它比使用查询的解决方案更长,但您可能会发现此过程代码更易于理解和维护。当然执行很方便:

CALL process_test_limit('AAA');

它是如何工作的?该过程从test_limit开始按folder排序,并跟踪id,直到运行总计达到400或folder更改为止。如果文件夹中已有status'in_process'的记录,则该文件夹将被忽略。

DROP PROCEDURE IF EXISTS `process_test_limit`;
DELIMITER $$
CREATE PROCEDURE `process_test_limit` (IN p_request_id VARCHAR(32))
BEGIN

    DECLARE v_sqlsafeupdates  BOOLEAN;  -- State of SQL_SAFE_UPDATES at execution start
    DECLARE v_to_update       CHAR(64); -- Name of temp table to store IDs of rows to be updated
    DECLARE v_id              INT;
    DECLARE v_folder          VARCHAR(255);
    DECLARE v_size            BIGINT UNSIGNED;
    DECLARE v_running_total   BIGINT UNSIGNED  DEFAULT 0;
    DECLARE v_prev_id         INT              DEFAULT NULL;
    DECLARE v_prev_folder     VARCHAR(255)     DEFAULT NULL;

    -- Cursor end handler flag (must be declared before cursors)
    DECLARE v_cursor_end   BOOLEAN DEFAULT FALSE;

    -- Main cursor to iterate through the rows of test_limit
    DECLARE c_test_limit CURSOR FOR
    SELECT tl.id
         , tl.folder
         , tl.size
      FROM test_limit tl
     WHERE tl.status = 'awaiting'
       AND NOT EXISTS (SELECT 1 
                         FROM test_limit tl_check
                        WHERE tl_check.folder = tl.folder
                          AND tl_check.status = 'in_progress'
                        LIMIT 1
                      )
     ORDER BY tl.folder -- Order is important: we process max one folder per call
            , tl.size
    ;

    -- Cursor end handler
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_cursor_end = TRUE;

    -- Remember the current state of SQL_SAFE_UPDATES, then disable it
    SET v_sqlsafeupdates = @@sql_safe_updates;
    SET @@sql_safe_updates = FALSE;

    -- Create temp table for tracking IDs of rows to update
    SET v_to_update = CONCAT(
        'process_test_limit_', CAST(UNIX_TIMESTAMP() AS CHAR), '_tmp'
    );
    SET @create_tmp_table_sql = CONCAT(
        'CREATE TEMPORARY TABLE ', v_to_update, 
        ' (id INT NOT NULL PRIMARY KEY) ENGINE=MEMORY'
    );
    PREPARE create_tmp_table_stmt FROM @create_tmp_table_sql;
    EXECUTE create_tmp_table_stmt;
    DEALLOCATE PREPARE create_tmp_table_stmt;

    -- Prepare statement for saving IDs into "to update" tmp table
    SET @save_id_sql = CONCAT('INSERT INTO ', v_to_update, ' (id) VALUES (?)');
    PREPARE save_id_stmt FROM @save_id_sql;

    -- Open the cursor to enable us to read the ordered result set one record at a time
    OPEN c_test_limit;

    -- Process the ordered test_limit records one-by-one
    l_test_limit: LOOP

        -- Get the next record (advance the cursor)
        FETCH c_test_limit
         INTO v_id, v_folder, v_size
        ;

        -- Exit the loop if there are no more records to process
        IF v_cursor_end THEN
            LEAVE l_test_limit;
        END IF;

        -- First/same-as-last folder and running total not over 400? Save ID for update.
        IF (v_prev_folder IS NULL OR v_folder = v_prev_folder) AND v_running_total + v_size <= 400 THEN

            SET @id = CAST(v_id AS CHAR);
            EXECUTE save_id_stmt USING @id;

            -- Set variables for next iteration
            SET v_prev_id = v_id;
            SET v_prev_folder = v_folder;
            SET v_running_total = v_running_total + v_size;

        -- Different folder or running total over 400? Exit loop.
        ELSE

            LEAVE l_test_limit;

        END IF;

    END LOOP;

    -- Deallocate statement for inserting rows into temp table
    DEALLOCATE PREPARE save_id_stmt;

    -- Update rows
    SET @update_sql = CONCAT(
        'UPDATE test_limit t INNER JOIN ', v_to_update, ' tmp',
            ' ON t.id = tmp.id',
            ' SET t.status = ?,',
            ' t.request_id = ?'
    );
    SET @status = 'in_progress';
    SET @request_id = p_request_id;
    PREPARE update_stmt FROM @update_sql;
    EXECUTE update_stmt USING @status, @request_id;
    DEALLOCATE PREPARE update_stmt;

    -- Drop temp table
    SET @drop_tmp_table_sql = CONCAT('DROP TEMPORARY TABLE ', v_to_update);
    PREPARE drop_tmp_table_stmt FROM @drop_tmp_table_sql;
    EXECUTE drop_tmp_table_stmt;
    DEALLOCATE PREPARE drop_tmp_table_stmt;

    -- Return SQL_SAFE_UPDATES to its original state at execution start
    SET @@sql_safe_updates = v_sqlsafeupdates;

END$$
DELIMITER ;

结果似乎满足您的要求:

-- Execution 1: 'AAA'
CALL process_test_limit('AAA');
SELECT * FROM test_limit;
-- id, folder, status, size, request_id
-- 1, /tmp/AAA/bar, in_progress, 200, AAA
-- 2, /tmp/AAA/bar, in_progress, 200, AAA
-- 3, /tmp/AAA/bar, awaiting, 200, 
-- 4, /tmp/BBB/bar, awaiting, 200, 
-- 5, /tmp/BBB/bar, awaiting, 200, 

-- Execution 2: 'BBB'
CALL process_test_limit('BBB');
SELECT * FROM test_limit;
-- id, folder, status, size, request_id
-- 1, /tmp/AAA/bar, in_progress, 200, AAA
-- 2, /tmp/AAA/bar, in_progress, 200, AAA
-- 3, /tmp/AAA/bar, in_progress, 200,
-- 4, /tmp/BBB/bar, awaiting, 200, BBB
-- 5, /tmp/BBB/bar, awaiting, 200, BBB

-- Execution 3: 'CCC'
CALL process_test_limit('CCC');
SELECT * FROM test_limit;
-- id, folder, status, size, request_id
-- 1, /tmp/AAA/bar, in_progress, 200, AAA
-- 2, /tmp/AAA/bar, in_progress, 200, AAA
-- 3, /tmp/AAA/bar, in_progress, 200,
-- 4, /tmp/BBB/bar, in_progress, 200, BBB
-- 5, /tmp/BBB/bar, in_progress, 200, BBB

答案 1 :(得分:1)

让我花了一些时间来思考逻辑。这是sql fiddle http://sqlfiddle.com/#!9/227dd0/1

UPDATE test_limit u
JOIN
(
  SELECT
    t1.*
    ,f.NonAwaitingFolderTotal
    ,(@runtot := @runtot + t1.size) as RunningTotal
  FROM
    (
      SELECT
        folder
        ,SUM(CASE WHEN status <> 'awaiting' THEN size ELSE 0 END) as NonAwaitingFolderTotal
      FROM
        test_limit t
      GROUP BY
        folder
      HAVING
        SUM(CASE WHEN status <> 'awaiting' THEN size ELSE 0 END) <= 400
      ORDER BY
        NonAwaitingFolderTotal, folder
      LIMIT 1
    ) f
    INNER JOIN test_limit t1
    ON f.folder = t1.folder
    CROSS JOIN (SELECT @runtot:=0) var
  WHERE
    t1.status = 'awaiting'
)  t2
ON u.id = t2.id
AND (t2.NonAwaitingFolderTotal + t2.RunningTotal) <= 400
SET
  u.status = 'in_progress'
  ,u.request_id = @request_id
;

逻辑就像这样

  • 找出要使用的文件夹,找到该文件夹​​中当前的非等待总大小。然后按最低非等待大小(in_progress,创建)选择一个文件夹,如果按文件夹名称绑定,则限制为1。
  • 获取该文件夹中所有等待记录的运行总计,用于确定在达到允许的最大值之前可以更新哪些行。
  • 使用连接进行更新,以查看正在运行的总查询的结果,其中,非等待记录的总大小+该记录的运行总计小于最大值400。

只是因为我想把它放在某个地方,主要的问题是你在哪里使用的运行总量并没有按正确的等级进行分组。这里有几个运行总和&amp;行数函数我通过考虑它来工作。

SELECT 
  *
  ,(@foldercount := IF(@prevfolder=folder,@foldercount,@foldercount+1)) as FolderNum
  ,(@rownum := @rownum + 1) as RowNum
  ,(@grouprownum := IF(@prevfolder=folder,@grouprownum+1,1)) as GroupRowNum
  ,(@total := IF(@prevfolder=folder,@total + t.size,t.size)) as GroupRunningTotal
  ,(@GroupAwaitRunningTotal := IF(
        @prevfolder=folder
        ,IF(t.status = 'awaiting',@GroupAwaitRunningTotal + t.size,@GroupAwaitRunningTotal)
        ,IF(t.status = 'awaiting',t.size,0)
      )
   ) as GroupAwaitRunningTotal
   ,(@GroupNonAwaitRunningTotal := IF(
        @prevfolder=folder
        ,IF(t.status != 'awaiting',@GroupNonAwaitRunningTotal + t.size,@GroupNonAwaitRunningTotal)
        ,IF(t.status != 'awaiting',t.size,0)
      )
   ) as GroupNonAwaitRunningTotal
  ,(@runtot := @runtot + t.size) as RunningTotal
  ,@prevfolder:=folder
FROM 
  test_limit t
  CROSS JOIN
  (SELECT @prevfolder:=NULL, @GroupAwaitRunningTotal := 0
     ,@GroupNonAwaitRunningTotal := 0
     ,@total:=0, @rownum:=0, @grouprownum:=0, @runtot:=0, @foldercount:=0) var