Blog

  • Auto_increment gaps

    Why are there gaps in my auto_increment sequence, even if there are no deletes or rolled back transactions?

    Is it a bug?

    The manual says, “For lock modes 1 or 2, gaps may occur between successive statements because for bulk inserts the exact number of auto-increment values required by each statement may not be known and overestimation is possible.”

    Where does that overestimation come from?

    An example to illustrate:

    DROP TABLE IF EXISTS t;
    CREATE TABLE t (a bigint unsigned auto_increment primary key) ENGINE=InnoDB SELECT NULL AS a;
    /* #1 */ INSERT INTO t SELECT NULL FROM t;
    /* #2 */ INSERT INTO t SELECT NULL FROM t;
    /* #3 */ INSERT INTO t SELECT NULL FROM t;
    /* #4 */ INSERT INTO t SELECT NULL FROM t;
    SELECT * FROM t;

    +----+
    | a |
    +----+
    | 1 |
    | 2 |
    | 3 |
    | 4 |
    | 6 |
    | 7 |
    | 8 |
    | 9 |
    | 13 |
    | 14 |
    | 15 |
    | 16 |
    | 17 |
    | 18 |
    | 19 |
    | 20 |
    +----+
    16 rows in set (0.02 sec)

    Notice that 5 and 10-12 are missing. If we did another insert, we’d be missing 21-27 (try it and see!)

    Here’s a model of what MySQL is doing:

    Create the table and simultaneously insert a single row. That is the row where a=1.

    #1: Insert as many rows as there are in the table (it’s one row, but MySQL doesn’t know that.)
    – Grab a chunk of auto_increment values. How many in the chunk? One – the value ‘2’. Insert it (one row inserted).
    – No more rows to insert, so all done.

    #2: Insert as many rows as there are in the table (it’s two rows, but MySQL doesn’t know that.)
    – Grab a chunk of auto_increment values. How many in the chunk? One – the value ‘3’. Insert it (one row inserted).
    – Still more rows to insert. Grab another chunk, twice as big as before – two values, ‘4’ and ‘5’. Insert the ‘4’ (two rows inserted).
    – No more rows to insert. Discard the left over ‘5’.

    #3: Insert as many rows as there are in the table (it’s four rows, but MySQL doesn’t know that.)
    – Grab a chunk of auto_increment values. How many in the chunk? One – the value ‘6’. Insert it (one row inserted).
    – Still more rows to insert. Grab another chunk, twice as big as before – two values, ‘7’ and ‘8’. Insert them (three rows inserted).
    – Still more rows to insert. Grab another chunk, twice as big as before – four values, ‘9’, ’10’, ’11’, ’12’. Insert the ‘9’ (four rows inserted).
    – No more rows to insert. Discard the left over ’10’, ’11’, and ’12’.

    #4: Insert as many rows as there are in the table (it’s eight rows, but MySQL doesn’t know that.)
    – Grab a chunk of auto_increment values. How many in the chunk? One – the value ’13’. Insert it (one row inserted).
    – Still more rows to insert. Grab another chunk, twice as big as before – two values, ’14’ and ’15’. Insert them (three rows inserted).
    – Still more rows to insert. Grab another chunk, twice as big as before – four values, ’16’, ’17’, ’18’, ’19’. Insert them (seven rows inserted).
    – Still more rows to insert. Grab another chunk, twice as big as before – eight values, ’20’, ’21’, ’22’, …, ’27’. Insert the ’20’ (eight rows inserted).
    – No more rows to insert. Discard the left over ’21’, ’22’, etc.

    The gap can get as big as 65535 (I didn’t look in the code to confirm that, it’s just what running the test above a few more times seems to suggest).

    When innodb_autoinc_lock_mode=1, there can be gaps between statements, but not within a statement, because there’s a lock on the auto_increment until we’re done. #4 above is guaranteed to get 8 consecutive values, you just might not be sure where they are going to start (well, now you are, because you read this post).

    When innodb_autoinc_lock_mode=2, there can be gaps within a statement, because the auto_increment is not locked. Imagine we’re in the middle of #4 above. Our statement is inserting the ’14’ and ’15’, when another statement comes along wanting just a single auto_increment value. It gets the ’16’. Now it’s our turn to ask for another chunk, and we get ’17’, ’18’, ’19’, ’20’. While we’re doing those, another statement comes along and steals our ’21’. So the last row for our statement is ’22’.

  • MidPoint of a LineString

    CREATE FUNCTION MidPoint(ls LINESTRING)
    RETURNS POINT
    DETERMINISTIC
    BEGIN
    DECLARE len double;
    DECLARE workLength double DEFAULT 0;
    DECLARE workPoint int DEFAULT 1;
    DECLARE point0 POINT;
    DECLARE point1 POINT;
    DECLARE distanceOver double;
    DECLARE segmentLength double;

    IF (NumPoints(ls) = 1) THEN return PointN(ls, 1); END IF;

    -- find half the length of the linestring
    SET len := GLength(ls) / 2;

    -- walk the linestring until we exceed the distance
    WHILE (workLength < len) DO SET point0 = PointN(ls, workPoint); SET workPoint := workPoint + 1; SET point1 = PointN(ls, workPoint); SET segmentLength = GLength(LineString(point0, point1)); SET workLength := workLength + segmentLength; END WHILE; -- distance to backup SET distanceOver = workLength - len; -- midpoint is distanceOver back down the last segement RETURN POINT( X(point1) - distanceOver / segmentLength * (X(point1) - X(point0)), Y(point1) - distanceOver / segmentLength * (Y(point1) - Y(point0)) ); END

  • Quiz: A More Perfect UNION

    Today I saw a query like this:
    SELECT d FROM t;

    Performance was terrible. I ran out of patience after several minutes and killed the thread.

    I changed the query to this:
    (SELECT d FROM t) UNION ALL (SELECT NULL LIMIT 0);

    It completed in under 3 seconds.

    Can you explain how a no-op UNION so dramatically improved performance? (I couldn’t have, without help from Jesper Krogh and James Day).

    http://dev.mysql.com/doc/refman/5.5/en/cursor-restrictions.html
    The field `d` is a varchar and is bigger than it needs to be.
    http://dev.mysql.com/doc/refman/5.5/en/memory-storage-engine.html
    http://dev.mysql.com/doc/refman/5.5/en/internal-temporary-tables.html
    The query is in a cursor. MySQL materializes cursors to a temporary table, using the MEMORY engine if it can, then converting to MyISAM if it gets too big. The MEMORY engine doesn’t support variable length rows, so VARCHARs are converted to CHARs. The field `d` only contains one character in each row, but is defined as a VARCHAR(20000) – which means the temporary table will grow very large. Even when converted to MyISAM, it retains that fixed width format, so the temp table is huge.

    The “presence of any column larger than 512 bytes in the SELECT list, if UNION or UNION ALL is used,” prevents use of an in-memory temporary table. That means the temporary table will be created directly as MyISAM on the disk, and will keep the VARCHAR format, so it will be much smaller. Converting the field to TEXT would have the same effect.

  • Yet another way to count occurences of a substring in a string

    In response to http://forge.mysql.com/snippets/view.php?id=60 – adding support to count strings, not just characters

    (
      character_length(<string1>)
      - character_length(REPLACE(<string1>, <searchString>, '')
    ) / character_length(<searchString>)
  • Easter date

    Finds the date for Easter (Western) given a year.
    Sample usage: SELECT easter(YEAR(NOW()));

    DELIMITER //
    CREATE FUNCTION easter(inYear YEAR) RETURNS DATE DETERMINISTIC
    BEGIN
        DECLARE a, b, c, d, e, k, m, n, p, q INT;
    
        DECLARE easter DATE;
    
        SET k = FLOOR(inYear / 100);
        SET a = MOD(inYear, 19);
        SET b = MOD(inYear, 4);
        SET c = MOD(inYear, 7);
        SET q = FLOOR(k / 4);
        SET p = FLOOR((13 + 8 * k) / 25);
        SET m = MOD((15-p+k-q), 30);
        SET d = MOD((19 * a + m), 30);
        SET n = MOD((4+k-q), 7);
        SET e = MOD((2*b+4*c+6*d+n), 7);
    
        SET easter = CASE
            WHEN d + e <= 9 THEN CONCAT_WS('-', inYear, '03', 22 + d + e)
            WHEN d = 29 AND e = 6 THEN CONCAT_WS('-', inYear, '04-19')
            WHEN d = 28 AND e = 6 AND a > 10 THEN CONCAT_WS('-', inYear, '04-18')
            ELSE CONCAT_WS('-', inYear, '04', LPAD(d + e - 9, 2, 0))
        END;
    
        RETURN easter;
    END
    //
    DELIMITER ;
  • Show all grants

    A stored procedure to show all grants in the database.

    USE mysql;
    
    DELIMITER //
    CREATE PROCEDURE showAllGrants() BEGIN
        DECLARE done INT DEFAULT 0;
        DECLARE theUser CHAR(16);
        DECLARE theHost CHAR(60);
        DECLARE cur1 CURSOR FOR SELECT user, host FROM mysql.user;
        DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done = 1;
        OPEN cur1;
    
        REPEAT
            FETCH cur1 INTO theUser, theHost;
            IF NOT done THEN
                SET @sql := CONCAT('SHOW GRANTS FOR ', QUOTE(theUser), '@', QUOTE(theHost));
                PREPARE grantStatement FROM @sql;
                EXECUTE grantStatement;
                DROP PREPARE grantStatement;
            END IF;
        UNTIL done END REPEAT;
    
        CLOSE cur1;
    END//
    DELIMITER ;
    
    CALL showAllGrants();
  • Roman numerals

    Convert an integer in the range 0 to 3999 to Roman numerals.

    CREATE FUNCTION `toRoman`(inArabic int unsigned) RETURNS varchar(15) CHARSET latin1 DETERMINISTIC
    BEGIN
        DECLARE numeral CHAR(7) DEFAULT 'IVXLCDM';
    
        DECLARE stringInUse CHAR(3);
        DECLARE position tinyint DEFAULT 1;
        DECLARE currentDigit tinyint;
    
        DECLARE returnValue VARCHAR(15) DEFAULT '';
    
        IF(inArabic > 3999) THEN RETURN 'overflow'; END IF;
        IF(inArabic = 0) THEN RETURN 'N'; END IF;
    
        WHILE position <= CEIL(LOG10(inArabic + .1)) DO
            SET currentDigit := MOD(FLOOR(inArabic / POW(10, position - 1)), 10);
    
            SET returnValue := CONCAT(
                CASE currentDigit 
                    WHEN 4 THEN CONCAT(SUBSTRING(numeral, position * 2 - 1, 1), SUBSTRING(numeral, position * 2, 1))
                    WHEN 9 THEN CONCAT(SUBSTRING(numeral, position * 2 - 1, 1), SUBSTRING(numeral, position * 2 + 1, 1))
                    ELSE CONCAT(
                        REPEAT(SUBSTRING(numeral, position * 2, 1), currentDigit >= 5),
                        REPEAT(SUBSTRING(numeral, position * 2 - 1, 1), MOD(currentDigit, 5))
                    )
                END,
                returnValue);
    
            SET position := position + 1;
        END WHILE;
        RETURN returnValue;
    END
  • SHOW CREATE TRIGGER 5.0

    SHOW CREATE TRIGGER for 5.0 – 5.1.20

    CREATE FUNCTION SHOW_CREATE_TRIGGER(in_trigger varchar(255)) 
    RETURNS text READS SQL DATA 
    BEGIN
        DECLARE returnText text;
        SELECT CONCAT_WS(
            " ",
            "CREATE TRIGGER", 
            TRIGGER_NAME, 
            ACTION_TIMING, 
            EVENT_MANIPULATION, 
            "ON", 
            EVENT_OBJECT_TABLE, 
            "FOR EACH ROW", 
            ACTION_STATEMENT) into returnText
        FROM information_schema.triggers;
    RETURN returnText;
    END
  • Reverse Roman Numerals

    Translate from Roman Numerals back to decimal. Legal range is 0 to 3999 (that is, N to MMMCMXCIX) You can use IV or IIII for 4. You could even use a string of twenty I’s for 20, but there’s a string limit of 15, since that’s the length of the biggest well-formed numeral below 3999.

    CREATE FUNCTION fromRoman (inRoman varchar(15)) RETURNS integer DETERMINISTIC
    BEGIN
    
        DECLARE numeral CHAR(7) DEFAULT 'IVXLCDM';
    
        DECLARE digit TINYINT;
        DECLARE previous INT DEFAULT 0;
        DECLARE current INT;
        DECLARE sum INT DEFAULT 0;
    
        SET inRoman = UPPER(inRoman);
    
        WHILE LENGTH(inRoman) > 0 DO
            SET digit := LOCATE(RIGHT(inRoman, 1), numeral) - 1;
            SET current := POW(10, FLOOR(digit / 2)) * POW(5, MOD(digit, 2));
            SET sum := sum + POW(-1, current < previous) * current;
            SET previous := current;
            SET inRoman = LEFT(inRoman, LENGTH(inRoman) - 1);
        END WHILE;
    
        RETURN sum;
    END
  • Extract range characters

    Given a source string and a two-character range string, extracts all characters from the source that fall within the range. Sort of a simple version of the [A-Z] syntax from regular expressions.

    Examples:
    extractRange(‘123-ABC-456’, ‘AZ’) returns ‘ABC’
    extractRange(‘123-ABC-456′, ’09’) returns ‘123456’

    CREATE FUNCTION extractRange(inString TEXT, inRange char(2)) 
    RETURNS TEXT 
    DETERMINISTIC
    BEGIN
    DECLARE returnString TEXT DEFAULT '';
    DECLARE pointer INT DEFAULT 1;
    WHILE pointer <= CHAR_LENGTH(inString) DO
        IF MID(inString, pointer, 1) BETWEEN LEFT(inRange, 1) AND RIGHT(inRange, 1) THEN
            SET returnString := CONCAT(returnString, MID(inString, pointer, 1));
        END IF;
        SET pointer := pointer + 1;
    END WHILE;
    return returnString;
    END