├── LICENSE ├── Makefile ├── README.md ├── chapters ├── indexes.md ├── indexes │ ├── duplicate_indexes.sql │ ├── foreign_keys.sql │ ├── fulltext_indexes.sql │ ├── functional_indexes.sql │ ├── indexing_for_wildcard_searches.sql │ ├── indexing_json_columns.sql │ ├── invisible_indexes.sql │ ├── primary_key.sql │ ├── secondary_keys.sql │ └── where_to_add_indexes.sql ├── queries.md ├── queries │ ├── CTEs.sql │ ├── counting_results.sql │ ├── dealing_with_nulls.sql │ ├── explain.sql │ ├── explain_analyze.sql │ ├── index_ofuscation.sql │ ├── indexing_joins.sql │ ├── joins.sql │ ├── limiting_rows.sql │ ├── recursive_CTEs.sql │ ├── redundant_and_approximate_conditions.sql │ ├── sorting_and_limiting.sql │ ├── subqueries.sql │ └── unions.sql ├── schema.md └── schema │ ├── binary_strings.sql │ ├── decimal.sql │ ├── enums.sql │ ├── generated_columns.sql │ ├── json.sql │ └── strings.sql ├── docker-compose.yml └── examples ├── bitswise_operations.sql ├── chaining_rows.sql ├── deferred_joins.sql ├── geographical_searches.sql ├── md5_column.sql ├── md5_multiple_columns.sql ├── meta_tables.sql ├── offset_limit_pagination.sql ├── summary_tables.sql └── timestamp_vs_booleans.sql /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Pedro López Mareque 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | 3 | .PHONY: help 4 | help: ## Show this help. 5 | @grep -E '^\S+:.*?## .*$$' $(firstword $(MAKEFILE_LIST)) | \ 6 | awk 'BEGIN {FS = ":.*?## "}; {printf "%-30s %s\n", $$1, $$2}' 7 | 8 | .PHONY: up 9 | up: ## Start all the Docker services 10 | docker compose up -d 11 | 12 | .PHONY: down 13 | down: ## Stop and remove all the Docker services, volumes and networks 14 | docker compose down -v --remove-orphans 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [SQL for Developers](https://planetscale.com/learn/courses/mysql-for-developers/introduction/course-introduction) 2 | 3 | ## Pre-requirements 4 | 5 | - You need to have [Docker](https://www.docker.com/get-started/) installed in your computer. 6 | - In order to have the MySQL database running you should run `make up` 7 | in the terminal. 8 | - In order to stop the MySQL database you should run `make up` 9 | in the terminal. 10 | 11 | ## Chapters 12 | 13 | - ### [Schema](chapters/schema.md) 14 | 15 | - ### [Indexes](chapters/indexes.md) 16 | 17 | - ### [Queries](chapters/queries.md) 18 | 19 | - ### [Examples](examples/) 20 | 21 | -------------------------------------------------------------------------------- /chapters/indexes.md: -------------------------------------------------------------------------------- 1 | # [Indexes](https://planetscale.com/learn/courses/mysql-for-developers/indexes) 2 | 3 | ## Introduction 4 | 5 | - Indexing is the best way to ensure that your queries perform well. 6 | - Indexes are an entirely separate data structure that maintain a copy of part of your data. 7 | - When you create an index, it creates a second data structure, which is different from your primary data structure. 8 | - Each index maintains a copy of part of your data. 9 | - You should also create as few as you can get away with because creating too many indexes can impact the performance. 10 | - You have to examine your queries to determine which indexes will perform the best. 11 | - Indexing should be driven by access patterns. 12 | 13 | ## Primary Keys 14 | 15 | - A primary key is a unique identifier for each row in the table. 16 | - A secondary key is any index that is not the primary key. 17 | - The primary key determines how your data is stored on disk. 18 | 19 | ## Secondary Keys 20 | 21 | - A secondary key is simply any index that is not the primary key of a table. 22 | - Every MySQL table has one primary key and can have multiple secondary keys. 23 | - Every secondary key has the primary key appended to it, as each leaf node in the secondary key contains a pointer back to the row. 24 | 25 | ## Primary Key Data types 26 | 27 | - The recommended practice for primary keys is to use unsigned big integers. 28 | - Unsigned big integers provide virtually infinite room to grow. 29 | - Auto-incrementing preserves a natural order for the records. 30 | - Choosing a string data type, such as a `UUID` or a `GUID`, as a primary key can be tempting, but it has potential pitfalls. 31 | 32 | ## Where to add indexes 33 | 34 | - Your queries should drive your indexes. 35 | - Do not create an index on every column. 36 | - The most basic use of an index is for direct access (`where`). 37 | - Indexes can also be used for unbounded and bounded ranges (`<=>`, `between`). 38 | - Indexes can also be used to sort rows instead of having MySQL do a full table scan (`sort`). 39 | - Indexes can also be used to group rows together for an aggregate function (`group by`). 40 | 41 | ## Indexes Selectivity 42 | 43 | - **Cardinality** refers to the number of distinct values in a particular column that an index covers. 44 | - **Selectivity**, on the other hand, refers to how unique the values in a column are. 45 | 46 | ## Prefix indexes 47 | 48 | - By indexing just a part of a string column, you can make the index much smaller and faster. 49 | - Prefix indexing is especially suitable for long, evenly distributed, and unique strings, such as UUIDs and hashes. 50 | - To determine the prefix length of a column to index, we have to calculate the overall selectivity of the column and compare it to the selectivity of the prefix. 51 | - As you increase the prefix length of columns values, we'll notice that the selectivity will also increase. 52 | - Prefix indexes are not suitable for ordering or grouping. 53 | - The index does not contain the full string value, and therefore cannot be used. 54 | 55 | ## Composite indexes 56 | 57 | - Composite indexes cover multiple columns instead of having individual indexes on each column. 58 | - There are two main rules for using composite indexes: 59 | - **Left-to-right**, no skipping. 60 | - **Stops at the first range**. 61 | - Tips for defining composite indexes: 62 | - Equality conditions that are commonly used would be good candidates for being first in a composite index. 63 | - Range conditions or less frequently used columns would be better candidates for ordering later in the composite index. 64 | 65 | ## Covering indexes 66 | 67 | - A covering index is a regular index that provides all the data required for a query without having to access the actual table. 68 | - They eliminate the need for the engine to access the actual table, saving a secondary traversal to gather the rest of the data. 69 | - For an index to be considered a covering index, it must have all the data needed for a particular query. 70 | - The columns being selected. 71 | - The columns being filtered on. 72 | - The columns being used for sorting. 73 | 74 | ## Functional indexes 75 | 76 | - Function-based indexes are used in cases where you need to create an index on a function rather than a column. 77 | - A function-based index is created by applying a function to one or more columns of a table, and then creating an index on the results of that function. 78 | - Function-based indexes are particularly useful in scenarios where the thing that you're trying to index is not a column, but rather the result of some set of operations or functions. 79 | 80 | ## Indexing JSON columns 81 | 82 | - MySQL provides two viable methods to index specific keys out of a JSON blob: 83 | - Generating a column. 84 | - Creating a function-based index. 85 | - You can't just index a JSON blob because MySQL doesn't support indexing JSON blobs. 86 | 87 | ## Indexing for wildcard searches 88 | 89 | - MySQL can only use an index up until it reaches a wildcard character, such as %. 90 | - MySQL cannot use an index when a wildcard character is at the beginning of a search string. 91 | - While B-tree indexes work well for wildcard searches at the end of a search string, they may not be sufficient for more complex text searches. 92 | - In these cases, we can use a full text index. 93 | - Full text indexes allow us to search for specific words or phrases within a larger text column with much greater efficiency than using a simple wildcard search. 94 | 95 | ## Fulltext indexes 96 | 97 | - To add a full-text index to a table in MySQL, you can use an ALTER TABLE statement. 98 | - The use of the `FULLTEXT` keyword to create a full-text index instead of a regular B-tree index. 99 | - By default, full-text searches in MySQL are done in natural language mode. 100 | - For more advanced full-text searches, you can switch to boolean mode. 101 | - Boolean mode allows you to use modifiers, like `+, -, >, <` and parentheses in your search query 102 | - When using natural language mode, MySQL automatically orders the results by their relevancy score. 103 | - MySQL returns the relevancy score as part of the search query results. 104 | 105 | ## Invisible indexes 106 | 107 | - Making an index invisible allows you to monitor how your queries perform without the index without having to rebuild it. 108 | - If everything goes well, you can drop the index. 109 | - Making an index invisible reduces the risks and potential complications of dropping an index. 110 | - Making an index invisible enables you to test your queries without risking data loss or any adverse impact on system performance. 111 | 112 | ## Duplicate indexes 113 | 114 | - It's important to note that removing a redundant index can have unintended consequences, especially if you depend on the ordering of the rows in that index. 115 | - To prevent duplicate indexes from occurring in the first place, it's important to keep an eye out for indexes that have overlapping leftmost prefixes. 116 | 117 | ## Foreign Keys 118 | 119 | - A foreign key is a column or set of columns in a table that references the primary key of another table. 120 | - A foreign key constraint is a condition that ensures the referential integrity of the data by enforcing a relationship between the foreign key and the referenced primary key. 121 | - Constraints also require additional computation to maintain. 122 | - Foreign keys are an important tool for maintaining relationships and ensuring the integrity of data in a relational database. 123 | - By linking tables together and enforcing referential integrity, foreign keys help ensure consistency and accuracy in data management. 124 | 125 | -------------------------------------------------------------------------------- /chapters/indexes/duplicate_indexes.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE people ADD INDEX first_name (first_name); 2 | ALTER TABLE people ADD INDEX full_name (first_name, last_name, birthday); 3 | 4 | ALTER TABLE people ALTER INDEX first_name INVISIBLE; 5 | 6 | SELECT * FROM people WHERE first_name = 'Aaron' ORDER BY id DESC; 7 | 8 | -------------------------------------------------------------------------------- /chapters/indexes/foreign_keys.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE parent ( 2 | ID BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY 3 | ); 4 | 5 | CREATE TABLE child ( 6 | ID BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, 7 | parent_id BIGINT UNSIGNED, 8 | 9 | FOREIGN KEY (parent_id) REFERENCES parent(ID) 10 | ); 11 | 12 | INSERT INTO child (parent_id) VALUES (1); 13 | 14 | INSERT INTO parent (ID) VALUES (1); 15 | 16 | INSERT INTO child (parent_id) VALUES (1); 17 | 18 | DELETE FROM parent WHERE ID = 1; 19 | -------------------------------------------------------------------------------- /chapters/indexes/fulltext_indexes.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE people ADD FULLTEXT INDEX `fulltext`(first_name, last_name, bio); 2 | 3 | SELECT * FROM people WHERE MATCH(first_name, last_name, bio) AGAINST('Aaron'); 4 | 5 | SELECT * FROM people 6 | WHERE MATCH(first_name, last_name, bio) AGAINST('+Aaron -Francis' IN BOOLEAN MODE); 7 | -------------------------------------------------------------------------------- /chapters/indexes/functional_indexes.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE people ( 2 | id INT(11) NOT NULL AUTO_INCREMENT, 3 | first_name VARCHAR(50) NOT NULL, 4 | last_name VARCHAR(50) NOT NULL, 5 | birthday DATE NOT NULL, 6 | PRIMARY KEY (id) 7 | ); 8 | 9 | ALTER TABLE people ADD INDEX idx_month_birth ((MONTH(birthday))); 10 | 11 | -------------------------------------------------------------------------------- /chapters/indexes/indexing_for_wildcard_searches.sql: -------------------------------------------------------------------------------- 1 | SELECT * FROM people WHERE email LIKE 'aaron%'; 2 | 3 | ALTER TABLE people ADD INDEX (email); 4 | 5 | SELECT * FROM people WHERE email LIKE 'aaron%'; 6 | EXPLAIN SELECT * FROM people WHERE email LIKE 'aaron%'; 7 | 8 | SELECT * FROM people WHERE email LIKE '%aaron%'; 9 | EXPLAIN SELECT * FROM people WHERE email LIKE '%aaron%'; 10 | 11 | ALTER TABLE people ADD COLUMN email_domain VARCHAR(255) AS (SUBSTRING_INDEX(email, '@', -1)); 12 | ALTER TABLE people ADD INDEX (email_domain); 13 | 14 | EXPLAIN SELECT * FROM people WHERE email_domain = 'example.com'; 15 | SELECT * FROM people WHERE email_domain = 'example.com'; 16 | -------------------------------------------------------------------------------- /chapters/indexes/indexing_json_columns.sql: -------------------------------------------------------------------------------- 1 | -- Method 1: generating a column 2 | 3 | ALTER TABLE json_data ADD COLUMN email VARCHAR(255) GENERATED ALWAYS AS (`json` ->> '$.email'); 4 | 5 | ALTER TABLE json_data ADD INDEX (email); 6 | 7 | -- Method 2: Function-based index 8 | 9 | ALTER TABLE json_data ADD INDEX (( 10 | CAST(`json`->>'$.email') AS CHAR(255) COLLATE utf8mb4_bin) 11 | )); 12 | -------------------------------------------------------------------------------- /chapters/indexes/invisible_indexes.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE people ALTER INDEX email_idx INVISIBLE; 2 | 3 | ALTER TABLE people ALTER INDEX email_idx VISIBLE; 4 | -------------------------------------------------------------------------------- /chapters/indexes/primary_key.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | ID BIGINT PRIMARY KEY, 3 | ); 4 | -------------------------------------------------------------------------------- /chapters/indexes/secondary_keys.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE people ADD INDEX (name); 2 | 3 | SELECT * FROM people WHERE name = 'Suzanne'; 4 | -------------------------------------------------------------------------------- /chapters/indexes/where_to_add_indexes.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE people ADD INDEX birthday_index (birthday); 2 | 3 | SELECT * FROM people WHERE birthday = '1989-02-14'; 4 | 5 | SELECT * FROM people WHERE birthday >= '2006-01-01'; 6 | 7 | SELECT * FROM people WHERE birthday BETWEEN '2006-01-01' AND '2006-12-31'; 8 | 9 | SELECT * FROM people ORDER BY birthday LIMIT 10; 10 | 11 | SELECT birthday, COUNT(*) FROM people GROUP BY birthday; 12 | -------------------------------------------------------------------------------- /chapters/queries.md: -------------------------------------------------------------------------------- 1 | # [Queries](https://planetscale.com/learn/courses/mysql-for-developers/queries) 2 | 3 | ## EXPLAIN overview 4 | 5 | - `EXPLAIN` is a statement provided by MySQL that helps us analyze how queries are executed in a database. 6 | - It shows the execution plan for a query. 7 | - This statement returns output that shows how MySQL accessed the table: 8 | - `ID`: A unique identifier for the query being executed. 9 | - `Select Type`: Tells us the type of select statement is being executed. 10 | - This can be **simple, primary, union** or a few others. 11 | - `Table`: The name of the table being accessed. 12 | - `Partitions`: Displays the partitions being accessed for the query (beyond the scope of this course). 13 | - `Type`: The kind of access MySQL used to retrieve the data. 14 | - This is one of the most important column values, and we'll discuss it in more detail later. 15 | - `Possible Keys`: The possible indexes that MySQL could use. 16 | - `Key`: The actual index that MySQL uses. 17 | - `Key Length`: Displays the length of the index used by MySQL. 18 | - `Ref`: The value being compared to the index. 19 | - `Rows`: An estimated number of rows that MySQL needs to examine to return the result. 20 | - `Filtered`: The estimated percentage of rows that match the query criteria. 21 | 22 | ## EXPLAIN access types 23 | 24 | - Const 25 | - The **const** access method is one of the most efficient. 26 | - `Const` access is only used when a primary key or unique index is in place, allowing MySQL to locate the necessary row with a single operation. When you see const in the type column, it's telling you that MySQL knows there is only one match for this query, making the operation as efficient as possible. 27 | - Ref 28 | - The **ref** access method is slightly less efficient than const, but still an excellent choice if the right index is in place. Ref access is used when the query includes an indexed column that is being matched by an equality operator. If MySQL can locate the necessary rows based on the index, it can avoid scanning the entire table, speeding up the query considerably. 29 | - Fulltext 30 | - MySQL provides an option to create full-text indexes on columns intended for text-based search queries. 31 | - The **fulltext** access method is used when a full-text index is in place and the query includes a full-text search. Fulltext access allows MySQL to search the index and return the results quickly. 32 | - Range 33 | - When you use range in the where clause, MySQL knows that it will need to look through a range of values to find the right data. MySQL will use the B-Tree index to traverse from the top of the tree down to the first value of the range. From there, MySQL consults the linked list at the bottom of the tree to find the rows with values in the desired range. It's essential to note that MySQL will examine every element in the range until a mismatch is found, so this can be slower than some of the other methods mentioned so far. 34 | - Index 35 | - The **index** access method indicates that MySQL is scanning the entire index to locate the necessary data. 36 | - `Index` access is the slowest access method listed so far, but it is still faster than scanning the entire table. When MySQL cannot use a primary or unique index, it will use index access if an index is available. 37 | - All 38 | - The **all** access method means that MySQL is scanning the entire table to locate the necessary data. 39 | - `All` is the slowest and least efficient access method, so it's one that you want to avoid as much as possible. MySQL may choose to scan the entire table when there is no suitable index, so this is an excellent opportunity to audit your indexing strategy. 40 | 41 | ## EXPLAIN ANALYZE 42 | 43 | - The explain statement provides several formats that you can use to analyze your queries in more detail. 44 | - Some of the commonly used explain formats are: 45 | - `tree`: Useful for providing more detail into the execution plan in a nested tree structure. 46 | - `JSON`: Provides a more detailed view of the same information as provided in the tree format. 47 | - `EXPLAIN ANALYZE`: Actually runs the query and provides detailed statistics on the query's execution plan. 48 | - **It's important to note that this format actually runs the query**. 49 | 50 | ## Index obfuscation 51 | 52 | - Obfuscating your columns made it difficult for MySQL to use indexes. 53 | - Always leave your column alone as much as possible. 54 | - Any changes you make to it make it more difficult to use an index effectively. 55 | - Move everything to the other side of the operator when possible. 56 | 57 | ## Redundant and approximate conditions 58 | 59 | - Redundant conditions refer to query conditions that logically cannot change the result set. 60 | - The key benefit of redundant conditions lies in the fact that they help unlock indexes without any changes to the table. 61 | - Redundant and approximate conditions offer a powerful tool for optimizing database queries. 62 | 63 | ## Limiting rows 64 | 65 | - If we want to count the number of rows in a table, we should not select all of the data and send it back to our application. 66 | - Calculations such as minimums, maximums, and averages should be done in the database instead of in our application. 67 | - Search for distinct values should be also be done in the database. 68 | - We should always put an `ORDER BY`, otherwise MySQL gets to decide how to order the rows, which can cause inconsistencies. 69 | 70 | ## Joins 71 | 72 | - Inner Join: 73 | - Takes the left table and the right table and matches them up together based on the criteria you specify. 74 | - It only returns results that have a link in both tables. 75 | - Left Join: 76 | - A left join returns all the records from the left table, and any matching records from the right table. 77 | - Right Join: 78 | - A right join returns all the records from the right table, and any matching records from the left table. 79 | - Full outer joins: 80 | - Which returns all rows from both tables, whether or not there's a match. 81 | - **MySQL doesn't have this feature**. 82 | 83 | ## Indexing joins 84 | 85 | - When MySQL joins tables together, it needs to figure out which rows from one table match which rows from the other table. 86 | - One way to do this is by doing a full table scan, which is slow and inefficient. The better way is to use an index on the related columns, which allows MySQL to quickly retrieve the matching rows and combine them. 87 | - By properly indexing the related columns between tables, you can significantly improve the performance of your queries. 88 | 89 | ## Subqueries 90 | 91 | - Runs a separate query inside your main query. 92 | - One advantage of using subqueries is that you don't have to join all the data together and perform a `DISTINCT` operation after trimming it down. 93 | 94 | ## Common Table Expressions (CTEs) 95 | 96 | - Is a SQL statement that can be referenced within the context of a larger query. 97 | - `CTEs` are supported in MySQL 8. 98 | - `CTEs` allow queries to be broken down into smaller parts. 99 | - `CTEs` can be created using the `WITH` keyword. 100 | 101 | ## Recursive CTEs 102 | 103 | - Recursive CTEs refer to themselves repeatedly to build up data. 104 | - We define a CTE using the `WITH` keyword and specify the `RECURSIVE` modifier. 105 | 106 | ## Unions 107 | 108 | - A `UNION` query is used to combine the results of two or more SELECT statements into a single result set. 109 | - Bear in mind that **the number of columns in all SELECT statements must be the same**. 110 | - Use `UNION ALL` instead of `UNION` to prevent MySQL from eliminating duplicate rows. 111 | 112 | ## Sorting and Limiting 113 | 114 | - Sorting your rows is not free. 115 | - It can take a significant amount of resources and time to sort large data sets. 116 | - If you don't need your rows in a certain order, don't order them. 117 | - To make the sorting deterministic, you should add more columns to the `ORDER BY` clause. 118 | - It's important to note that when you use the `OFFSET` clause, the sorted result must be produced first. 119 | 120 | ## Counting results 121 | 122 | - To count the number of rows in a table use the `COUNT(*)` function. 123 | - To count the number of non-null values in a column use the `COUNT(column_name)` function. 124 | - The `COUNT(if_statement)` function allows us to count values based on a specified condition. 125 | - An alternative approach is to use the `SUM()` function instead. 126 | 127 | ## Dealing with NULLs 128 | 129 | - To account for null values, we can use the null-safe equal operator `<=>`, also known as the "spaceship operator". 130 | - To compare a column with a null value, we can use the `is null` or `is not null` operators. 131 | - To represent null values in our queries we can use the `ifnull` statement. 132 | - Alternatively, we can use the `coalesce` function. 133 | - This function returns the first non-null value in a list of values. 134 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /chapters/queries/CTEs.sql: -------------------------------------------------------------------------------- 1 | WITH spend_last_6 AS ( 2 | SELECT 3 | customer_id, 4 | SUM(amount) AS total_spend 5 | FROM 6 | payment 7 | INNER JOIN customer ON customer.id = payment.customer_id 8 | WHERE 9 | store_id = 1 10 | AND payment_date > CURRENT_DATE - INTERVAL 6 MONTH 11 | GROUP BY 12 | cusomter_id 13 | ); 14 | 15 | SELECT * FROM spend_last_6; 16 | -------------------------------------------------------------------------------- /chapters/queries/counting_results.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | rental_date, DAYOFWEEK(rental_date) 3 | FROM 4 | rental; 5 | 6 | SELECT 7 | COUNT(IF(DAYOFWEEK(rental_date) IN (1, 7), 1, NULL)) AS weekend_rentals, 8 | COUNT(IF(DAYOFWEEK(rental_date) NOT IN (1, 7), 1, NULL)) AS weekday_rentals, 9 | COUNT(return_date) AS completed_rentals, 10 | COUNT(*) AS total_rentals 11 | FROM 12 | rental; 13 | 14 | SELECT 15 | SUM(DAYOFWEEK(rental_date) IN (1, 7)) AS weekend_rentals, 16 | SUM(DAYOFWEEK(rental_date) NOT IN (1, 7)) AS weekday_rentals, 17 | COUNT(return_date) AS completed_rentals, 18 | COUNT(*) AS total_rentals 19 | FROM 20 | rental; 21 | -------------------------------------------------------------------------------- /chapters/queries/dealing_with_nulls.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | IFNULL(language_id, 'I don’t know') 3 | FROM 4 | films; 5 | 6 | SELECT 7 | COALESCE(preferred_language_id, original_language_id) as language_id 8 | FROM 9 | films; 10 | -------------------------------------------------------------------------------- /chapters/queries/explain.sql: -------------------------------------------------------------------------------- 1 | EXPLAIN SELECT * FROM people; 2 | -------------------------------------------------------------------------------- /chapters/queries/explain_analyze.sql: -------------------------------------------------------------------------------- 1 | EXPLAIN SELECT * FROM people WHERE first_name = "Aaron" 2 | 3 | EXPLAIN FORMAT=TREE SELECT * FROM people WHERE first_name = "Aaron" 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /chapters/queries/index_ofuscation.sql: -------------------------------------------------------------------------------- 1 | -- BAD 2 | SELECT * FROM film WHERE length / 60 < 2; 3 | 4 | -- BETTER 5 | SELECT * FROM film WHERE length < 2 * 60; 6 | -------------------------------------------------------------------------------- /chapters/queries/indexing_joins.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | film.title, actor.first_name, actor.last_name 3 | FROM film 4 | LEFT JOIN film_actor ON film_actor.film_id = film.id 5 | LEFT JOIN actor ON actor.id = film_actor.actor_id 6 | WHERE 7 | film.id <= 10 8 | LIMIT 10; 9 | 10 | ALTER TABLE film_actor ADD INDEX idx_fk_film_id (film_id); 11 | 12 | EXPLAIN SELECT 13 | film.title, actor.first_name, actor.last_name 14 | FROM film 15 | LEFT JOIN film_actor ON film_actor.film_id = film.id 16 | LEFT JOIN actor ON actor.id = film_actor.actor_id 17 | WHERE 18 | film.id <= 10 19 | 20 | ALTER TABLE film_actor ALTER INDEX idx_film_id INVISIBLE; 21 | 22 | EXPLAIN SELECT 23 | film.title, actor.first_name, actor.last_name 24 | FROM film 25 | LEFT JOIN film_actor ON film_actor.film_id = film.id 26 | LEFT JOIN actor ON actor.id = film_actor.actor_id 27 | WHERE 28 | film.id <= 10 29 | -------------------------------------------------------------------------------- /chapters/queries/joins.sql: -------------------------------------------------------------------------------- 1 | SELECT * FROM store; 2 | SELECT * FROM staff; 3 | 4 | SELECT * FROM store 5 | INNER JOIN staff ON store.manager_staff_id = staff.id; 6 | 7 | SELECT * FROM store 8 | LEFT JOIN staff ON store.manager_staff_id = staff.id; 9 | 10 | SELECT * FROM store 11 | RIGHT JOIN staff ON store.manager_staff_id = staff.id; 12 | 13 | -------------------------------------------------------------------------------- /chapters/queries/limiting_rows.sql: -------------------------------------------------------------------------------- 1 | SELECT COUNT(*) FROM table_name; 2 | 3 | SELECT MIN(column_name) FROM table_name; 4 | SELECT MAX(column_name) FROM table_name; 5 | SELECT AVG(column_name) FROM table_name; 6 | 7 | SELECT DISTINCT column_name FROM table_name; 8 | 9 | SELECT * FROM table_name LIMIT 10 OFFSET 20; 10 | -------------------------------------------------------------------------------- /chapters/queries/recursive_CTEs.sql: -------------------------------------------------------------------------------- 1 | WITH RECURSIVE numbers AS ( 2 | SELECT 1 AS n -- Initial Condition 3 | UNION ALL 4 | SELECT n + 1 FROM numbers WHERE n < 10 -- Recursive Condition 5 | ) 6 | 7 | SELECT * FROM numbers; 8 | 9 | WITH RECURSIVE all_dates AS ( 10 | SELECT '2023-01-01' AS dt -- Initial Condition 11 | UNION ALL 12 | SELECT dt + INTERVAL 1 DAY FROM all_dates WHERE dt < '2023-12-31' -- Recursive Condition 13 | ) 14 | SELECT * FROM all_dates; 15 | 16 | WITH RECURSIVE all_dates AS ( 17 | SELECT '2023-01-01' AS dt -- Initial Condition 18 | UNION ALL 19 | SELECT dt + INTERVAL 1 DAY FROM all_dates WHERE dt < '2023-12-31' -- Recursive Condition 20 | ) 21 | 22 | SELECT 23 | dt, 24 | sum(ammount) 25 | FROM 26 | all_dates 27 | LEFT JOIN payments ON all_dates.dt = payments.date; 28 | 29 | -------------------------------------------------------------------------------- /chapters/queries/redundant_and_approximate_conditions.sql: -------------------------------------------------------------------------------- 1 | SELECT * FROM people WHERE id <= 5; 2 | 3 | SELECT * FROM people 4 | WHERE 5 | id <= 5 6 | AND 7 | id <= 10; 8 | 9 | SELECT * FROM todos 10 | WHERE 11 | ADDTIME(due_date, due_time) BETWEEN NOW() AND NOW() + INTERVAL 1 DAY; 12 | 13 | SELECT * FROM todos 14 | WHERE 15 | ADDTIME(due_date, due_time) BETWEEN NOW() AND NOW() + INTERVAL 1 DAY 16 | AND 17 | due_date BETWEEN CURRENT_DATE AND CURRENT_DATE + INTERVAL 1 DAY; 18 | -------------------------------------------------------------------------------- /chapters/queries/sorting_and_limiting.sql: -------------------------------------------------------------------------------- 1 | SELECT * FROM people LIMIT 10; 2 | 3 | SELECT id, birthday FROM people ORDER BY birthday; 4 | 5 | SELECT id, birthday FROM people ORDER BY birthday DESC; 6 | 7 | SELECT id, birthday FROM people ORDER BY birthday, id; 8 | 9 | SELECT id, birthday FROM people ORDER BY birthday, id LIMIT 100 OFFSET 20; 10 | 11 | -------------------------------------------------------------------------------- /chapters/queries/subqueries.sql: -------------------------------------------------------------------------------- 1 | SELECT * FROM customer 2 | INNER JOIN payment ON customer.id = payment.customer_id 3 | WHERE payment.amount > 5.99; 4 | 5 | SELECT * FROM customer 6 | WHERE 7 | id IN ( 8 | SELECT customer_id FROM payment WHERE amount > 5.99 9 | ); 10 | -------------------------------------------------------------------------------- /chapters/queries/unions.sql: -------------------------------------------------------------------------------- 1 | SELECT 1 2 | UNION 3 | SELECT 2; 4 | 5 | SELECT 6 | first_name, 7 | last_name, 8 | email_address 9 | FROM customers 10 | 11 | UNION ALL 12 | 13 | SELECT 14 | first_name, 15 | last_name, 16 | email_address 17 | FROM staff; 18 | -------------------------------------------------------------------------------- /chapters/schema.md: -------------------------------------------------------------------------------- 1 | # [Schema](https://planetscale.com/learn/courses/mysql-for-developers/schema) 2 | 3 | ## Introduction 4 | 5 | - Schema refers to the structure of your database tables, including column types, sizes, and attributes. 6 | - Three guiding principles in mind when choosing a data type: 7 | - Pick the **smallest** data type that will hold all of your data. 8 | - Pick the **simplest** column type that accurately reflects your data. 9 | - Ensure your schema accurately **reflects the reality** of your data. 10 | - The most common data types in MySQL: 11 | - `INT`: Used for integer values. 12 | - `FLOAT` and `DOUBLE`: Used for decimal or floating point values. 13 | - `VARCHAR`: Used for variable-length character strings. 14 | - `DATE`, `DATETIME` and `TIMESTAMP`: Used for date and time values. 15 | 16 | ## Integers 17 | 18 | - There are five data types that can be used to store integers: 19 | - `TINYINT`: Occupies one byte and can store values from 0 to 255 (or -128 to 127, if negative numbers are supported). 20 | - `SMALLINT`: Occupies two bytes and can store values from 0 to 65,535 (if negative numbers aren't supported). 21 | - `MEDIUMNINT`: Occupies three bytes and can store values from 0 to 16,777,215 (if negative numbers aren't supported). 22 | - `INT`: Occupies four bytes and can store values from 0 to 4,294,967,295 (if negative numbers aren't supported). 23 | - `BIGINT`: Occupies eight bytes and can store values from 0 to 18,446,744,073,709,551,615 (if negative numbers aren't supported). 24 | 25 | ## Decimals 26 | 27 | - There are four different types of data types in MySQL that can store decimal values: 28 | `DECIMAL`: a fixed-precision data type that stores exact values. 29 | `NUMERIC`: an alias for DECIMAL, the two are the same thing in MySQL. 30 | `FLOAT`: a floating-point data type that stores approximate values. 31 | `DOUBLE`: a floating-point data type that stores larger and more precise values than FLOAT. 32 | - If you need to store values that require absolute precision, such as currency or other financial data, you should use the **DECIMAL** data type. 33 | - You can specify the maximum number of digits and how many digits should occur after the decimal point. 34 | - `DECIMAL(10, 2)`: 10 digits in total, 2 of them are the decimals. 35 | - If you don't require precise decimal values, you can use either **FLOAT** or **DOUBLE**. 36 | - Both of these data types store approximate values 37 | 38 | ## Strings 39 | 40 | - There are many different types to choose from. Here's a list of the different types: 41 | - `CHAR` 42 | - `VARCHAR` 43 | - `TINYTEXT` 44 | - `TEXT` 45 | - `MEDIUMTEXT` 46 | - `LONGTEXT` 47 | - `BINARY` 48 | - `VARBINARY` 49 | - `TINYBLOB` 50 | - `BLOB` 51 | - `MEDIUMBLOB` 52 | - `LONGBLOB` 53 | - `ENUM` 54 | - `SET` 55 | - Fixed-length columns are declared using the `CHAR` data type and require you to specify the column size. 56 | - Variable-length columns are declared using the `VARCHAR` data type, and you have to specify the maximum column size. 57 | 58 | ## Binary Strings 59 | 60 | - The `BINARY` and `VARBINARY` columns store bytes only. 61 | - There is no character set or collation to be concerned about; it is just raw binary data. 62 | - The `BINARY` column is a fixed length column, while the `VARBINARY` column is a variable length column. 63 | - `BINARY` and `VARBINARY` columns in MySQL provide an efficient way to store binary data that may not have a valid string representation. 64 | - You can store `hash` and `UUID` data more compactly on disk, without the need for character sets and collations. 65 | 66 | ## Long Strings 67 | 68 | - `TEXT` and `BLOB` are used to store large amounts of text and binary data, respectively. 69 | - `TEXT` columns are used to store character data, such as strings of text. 70 | - Text columns are **not indexable**. 71 | - There are four types of text columns in MySQL: `TINYTEXT`, `TEXT`, `MEDIUMTEXT`, and `LONGTEXT`. 72 | - As the name suggests, each type has a cap for the amount of data it can hold. 73 | - `BLOB` columns are used to store binary data. 74 | - `BLOB` columns do not have a character set or a collation like `TEXT` columns do. 75 | - There are four types of blob columns: `TINYBLOB`, `BLOB`, `MEDIUMBLOB`, and `LONGBLOB`. 76 | - While `BLOB` columns can hold binary data such as images or audio files, it's not recommended to store them. 77 | - Best practices: 78 | - Only select the columns that you need. 79 | - Don't index or sort entire columns. 80 | - Use `VARCHAR` columns for smaller amounts of data 81 | 82 | ## Enums 83 | 84 | - Enums are a special data type that allows you to specify a predefined list of allowable values for a column. 85 | - Enums look like strings, but under the hood, they're stored as integers. 86 | - An enum column has a predefined list of allowable values, and any attempt to enter a value outside this list will result in an error. 87 | - Using enums in MySQL has several benefits: 88 | - Data validation: When attempting to enter an invalid value, an error is thrown. 89 | - Readability. 90 | - Compact data type. 91 | - There are some downsides too: 92 | - Changes to the schema: To add another option to the allowable values, you'll have to alter the schema. 93 | - Ordering: When sorting data using enums, MySQL sorts by the underlying integer value rather than the actual string. 94 | - Using integer enums. 95 | 96 | ## Dates 97 | 98 | - There are five different types you can use to store time-related data in MySQL: 99 | - `DATE`: If you only need to store the date. 100 | - `DATETIME`: If you need to store both the date and time (eight-byte data type). 101 | - `TIMESTAMP`: Same as `DATETIME` but with only four bytes. 102 | - It can store only from the year 1970 to 2038-01-19. 103 | - `YEAR`: If you need to store a year between 1901 and 2155. 104 | - `TIME`: Data type is used to store hours, minutes, and seconds. 105 | - `DATETIME` does not handle time zones at all. 106 | - `TIMESTAMP` tries to convert values to UTC when added to the database and back to your time zone when retrieved. 107 | 108 | ## JSON 109 | 110 | - MySQL has proper first-party support for `JSON`. 111 | - When working with JSON data, MySQL is much more strict than with other text data types. 112 | - The `->>` operator is used to extract a JSON object at a specific path. 113 | - `JSON` columns cannot be directly indexed. 114 | - You can create an index on a specific key within a JSON object, but not on the entire object itself. 115 | - `JSON` column might make sense when you're storing payloads. 116 | - One caveat when using `JSON` columns is that they can be quite heavy. 117 | 118 | ## Unexpected types 119 | 120 | - Booleans: 121 | - MySQL doesn't actually have a native `BOOLEAN` type. 122 | - Instead, MySQL uses a `TINYINT` column to simulate a boolean value. 123 | - IP addresses: 124 | - MySQL has a built-in function `INET_ATON()` that converts an IPv4 address to an integer. 125 | - `INET_NTOA()` to convert an integer back to an IP address. 126 | 127 | ## Generated columns 128 | 129 | - Generated columns are a way to make MySQL do more work on your behalf. 130 | - Created columns are based on other columns in your table. 131 | - They're computed by an expression, rather than being explicitly stored in the table. 132 | - Generated columns can be either virtual or stored. 133 | - A virtual column is calculated at runtime. 134 | - A stored column, on the other hand, is calculated during data insertion or update. 135 | - Use cases for generated columns: 136 | - Extracting data from JSON objects. 137 | - Performing calculations. 138 | - Normalizing data. 139 | 140 | ## Schema migrations 141 | 142 | - Migrations are a folder full of SQL statements that help keep track of changes to your database schema. 143 | - Best practices for migrations: 144 | - Always include explicit SQL statements to show how the database will move from one state to another. 145 | - Avoid using down migrations. 146 | - Utilize version control to keep track of changes to your schema over time. 147 | 148 | -------------------------------------------------------------------------------- /chapters/schema/binary_strings.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE bins ( 2 | bin BINARY(16), 3 | varbin VARBINARY(100) 4 | ); 5 | 6 | SELECT UNHEX(MD5('hello')); 7 | 8 | INSERT INTO bins (bin, varbin) VALUES (UNHEX(MD5('hello')), UNHEX(MD5('hello'))); 9 | 10 | SELECT * FROM bins; 11 | 12 | SELECT HEX(bin), HEX(varbin) FROM bins; 13 | -------------------------------------------------------------------------------- /chapters/schema/decimal.sql: -------------------------------------------------------------------------------- 1 | drop table decimals; 2 | 3 | CREATE TABLE decimals ( 4 | id INT AUTO_INCREMENT PRIMARY KEY, 5 | D1 DOUBLE, 6 | D2 DOUBLE 7 | ); 8 | 9 | INSERT INTO decimals (D1, D2) 10 | VALUES (100.4, 20.4), (-80.0, 0.0); 11 | 12 | SELECT * FROM decimals; 13 | 14 | SELECT SUM(D1), SUM(D2) FROM decimals; 15 | -------------------------------------------------------------------------------- /chapters/schema/enums.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE orders ( 2 | id INT AUTO_INCREMENT PRIMARY KEY, 3 | size ENUM('extra small', 'small', 'medium', 'large', 'extra large') 4 | ); 5 | 6 | INSERT INTO orders (size) VALUES ('small'), ('medium'), ('large'); 7 | 8 | SELECT size, size+0 FROM orders; 9 | -------------------------------------------------------------------------------- /chapters/schema/generated_columns.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE emails ( 2 | email varchar(255), 3 | domain varchar(255) AS (substring_index(email, '@', -1)) 4 | ); 5 | 6 | -------------------------------------------------------------------------------- /chapters/schema/json.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE has_json ( 2 | id INT(11) NOT NULL AUTO_INCREMENT, 3 | json JSON NULL, 4 | PRIMARY KEY (id) 5 | ); 6 | 7 | INSERT INTO has_json (json) VALUES ('{key:value}'); 8 | 9 | -- > Error: Invalid JSON text: "Missing a name for object member." at position 1 in value for column 'has_json.json'. 10 | 11 | INSERT INTO has_json (json) VALUES ('{"key": "value"}'); 12 | 13 | SELECT json FROM has_json; 14 | 15 | SELECT `json`->>"$.key" as key FROM has_json; 16 | 17 | ALTER TABLE has_json ADD INDEX my_index ((`json`->>"$.key")); 18 | -------------------------------------------------------------------------------- /chapters/schema/strings.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE strings ( 2 | fixed_five CHAR(5), 3 | fixed_32 CHAR(32) 4 | ); 5 | 6 | SHOW CREATE TABLE strings; 7 | 8 | CREATE TABLE strings ( 9 | variable_length VARCHAR(100) 10 | ); 11 | 12 | CREATE TABLE strings ( 13 | variable_length VARCHAR(100) CHARSET utf8mb4 COLLATE utf8mb4_general_ci 14 | ); 15 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | services: 3 | db: 4 | image: mysql 5 | restart: always 6 | environment: 7 | MYSQL_DATABASE: 'db' 8 | MYSQL_USER: 'user' 9 | MYSQL_PASSWORD: 'password' 10 | MYSQL_ROOT_PASSWORD: 'password' 11 | ports: 12 | - '3306:3306' 13 | -------------------------------------------------------------------------------- /examples/bitswise_operations.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users ADD COLUMN flags TINYINT UNSIGNED DEFAULT 0; 2 | 3 | SELECT * FROM users WHERE flags & 1 = 1; 4 | 5 | SELECT * FROM users WHERE flags & 17 = 17; 6 | -------------------------------------------------------------------------------- /examples/chaining_rows.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE imports ( 2 | id INT NOT NULL AUTO_INCREMENT, 3 | filename VARCHAR(255), 4 | owner INT DEFAULT 0, 5 | available TINYINT DEFAULT 1, 6 | started_at TIMESTAMP, 7 | finished_at TIMESTAMP, 8 | PRIMARY KEY (id), 9 | INDEX available_owner (available, owner) 10 | ); 11 | 12 | SELECT 13 | * 14 | FROM 15 | imports 16 | WHERE 17 | available = 1 18 | LIMIT 1; 19 | 20 | UPDATE imports 21 | SET 22 | owner = 32, -- unique worker id 23 | available = 0 24 | WHERE 25 | owner = 0 26 | AND 27 | available = 1 28 | LIMIT 1; 29 | 30 | SELECT 31 | * 32 | FROM 33 | imports 34 | WHERE 35 | owner = 32; 36 | -------------------------------------------------------------------------------- /examples/deferred_joins.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | * 3 | FROM 4 | people 5 | ORDER BY 6 | birthday, id 7 | LIMIT 20 8 | OFFSET 450000; 9 | 10 | SELECT id FROM people; 11 | 12 | SELECT * FROM people 13 | INNER JOIN ( 14 | SELECT id FROM people ORDER BY birthday, id LIMIT 20 OFFSET 450000 15 | ) AS people2 USING (id) 16 | ORDER BY 17 | birthday, id; 18 | 19 | ALTER TABLE people ADD INDEX birthday (birthday); 20 | 21 | -------------------------------------------------------------------------------- /examples/geographical_searches.sql: -------------------------------------------------------------------------------- 1 | SELECT stDistanceSphere( 2 | point(lat1, long1), 3 | point(lat2, long2) 4 | ); 5 | 6 | SELECT 7 | * 8 | FROM 9 | addresses 10 | WHERE 11 | ST_Distance_Sphere( 12 | POINT(-97.745363, 30.324014), 13 | POINT(longitude, latitude) 14 | ) < 1609; 15 | 16 | SELECT 17 | * 18 | FROM 19 | addresses 20 | WHERE 21 | latitude BETWEEN 30.30954084441 AND 30.33848715559 -- Bounding box latitude 22 | AND 23 | longitude BETWEEN -97.76213017291 AND -97.72859582708 -- Bounding box longitude 24 | AND 25 | ST_Distance_Sphere( 26 | point(-97.745363, 30.324014), 27 | point(longitude, latitude) 28 | ) <= 1609; 29 | 30 | ALTER TABLE addresses ADD INDEX idx_latitude (latitude); 31 | 32 | -------------------------------------------------------------------------------- /examples/md5_column.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE urls; 2 | 3 | CREATE TABLE urls ( 4 | ID BIGINT PRIMARY KEY AUTO_INCREMENT, 5 | url VARCHAR(1000) 6 | ); 7 | 8 | INSERT INTO urls (url) VALUES ('www.google.com'), ('specific_value'); 9 | 10 | SELECT * FROM urls WHERE url = 'specific_value'; 11 | 12 | SHOW INDEXES FROM urls; 13 | 14 | SELECT * FROM urls WHERE url_md5 = MD5('specific_value'); 15 | 16 | ALTER TABLE urls ADD COLUMN url_md5 binary(16) GENERATED ALWAYS AS (UNHEX(MD5(url))); 17 | 18 | ALTER TABLE urls ADD INDEX (url_md5); 19 | 20 | SELECT * FROM urls WHERE url_md5 = UNHEX(MD5('specific_value')); 21 | -------------------------------------------------------------------------------- /examples/md5_multiple_columns.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE addresses ADD COLUMN md5 BINARY(16) GENERATED ALWAYS AS ( 2 | UNHEX(MD5( 3 | CONCAT_WS('|', primary_line, secondary_line, urbanization, last line) 4 | )) 5 | ); 6 | 7 | ALTER TABLE addresses ADD UNIQUE INDEX (md5); 8 | 9 | -------------------------------------------------------------------------------- /examples/meta_tables.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | * 3 | FROM 4 | film_narrow 5 | INNER JOIN film_addendum ON film_narrow.id = film_addendum.film_id; 6 | 7 | -------------------------------------------------------------------------------- /examples/offset_limit_pagination.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | * 3 | FROM 4 | people 5 | ORDER BY 6 | birthday, 7 | id 8 | LIMIT 100 9 | OFFSET 0; 10 | 11 | -------------------------------------------------------------------------------- /examples/summary_tables.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | amount, 3 | YEAR(payment_date), 4 | MONTH(payment_date) 5 | FROM 6 | payments 7 | WHERE 8 | payment_date < DATE_FORMAT(CURRENT_DATE, '%Y-%m-01'); 9 | 10 | SELECT 11 | sum(amount) as amount, 12 | YEAR(payment_date) as `year`, 13 | MONTH(payment_date) as `month` 14 | FROM 15 | payments 16 | WHERE 17 | payment_date < DATE_FORMAT(CURRENT_DATE, '%Y-%m-01') 18 | GROUP BY 19 | `year`, `month`; 20 | 21 | CREATE TABLE payment_summary ( 22 | id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, 23 | amount DECIMAL(9,2), 24 | `year` YEAR, 25 | `month` TINYINT UNSIGNED 26 | ); 27 | 28 | INSERT INTO payment_summary (amount, year, month) SELECT 29 | sum(amount) as amount, 30 | YEAR(payment_date) as `year`, 31 | MONTH(payment_date) as `month` 32 | FROM 33 | payments 34 | WHERE 35 | payment_date < DATE_FORMAT(CURRENT_DATE, '%Y-%m-01') 36 | GROUP BY 37 | `year`, `month`; 38 | 39 | SELECT 40 | amount, 41 | year, 42 | month 43 | FROM 44 | payment_summary 45 | 46 | UNION ALL 47 | 48 | SELECT 49 | sum(amount) as amount, 50 | YEAR(payment_date) as `year`, 51 | MONTH(payment_date) as `month` 52 | FROM 53 | payments 54 | WHERE 55 | payment_date >= DATE_FORMAT(CURRENT_DATE, '%Y-%m-01'); 56 | 57 | WITH payment_data AS ( 58 | SELECT 59 | amount, 60 | year, 61 | month 62 | FROM 63 | payment_summary 64 | 65 | UNION ALL 66 | 67 | SELECT 68 | sum(amount) as amount, 69 | YEAR(payment_date) as `year`, 70 | MONTH(payment_date) as `month` 71 | FROM 72 | payments 73 | WHERE 74 | payment_date >= DATE_FORMAT(CURRENT_DATE, '%Y-%m-01') 75 | ); 76 | 77 | SELECT * FROM payment_data; 78 | -------------------------------------------------------------------------------- /examples/timestamp_vs_booleans.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE posts( 2 | title VARCHAR(125), 3 | is_archived BOOLEAN 4 | ); 5 | 6 | SELECT * FROM posts WHERE is_archived = false; 7 | 8 | CREATE TABLE posts( 9 | title VARCHAR(125), 10 | archived_at timestamp null 11 | ); 12 | 13 | SELECT * FROM posts WHERE archived_at IS NULL; 14 | 15 | --------------------------------------------------------------------------------