Because no software developer is complete without at least a passing knowledge of the world’s most popular query language Credit: Jamesbrey / Getty Images If yo were raised on MongoDB or learned full stack development in a coding bootcamp, then you might not know SQL. You might have even been told that SQL was bad. However, NoSQL databases are a bit like Hadoop — they had early promise but it fizzled. After 10 years of the NoSQL “revolution,” SQL databases remain the bulk of the database market. There are several reasons for this. First, many applications require real transactional integrity, which NoSQL databases (despite their claims) do not offer. Second, the relational model is an incredibly useful way to represent data. Third, SQL is still the best-thought-out and most capable query language. Fourth, GraphQL and object-relational mapping (ORM) technologies made developer challenges with relational databases largely moot. And finally, we have the emergence of distributed SQL databases, sometimes called NewSQL databases. (Full disclosure: I work for Yugabyte, provider of an open source distributed SQL database.) With the COVID-19 pandemic, transactional applications that never would have been trusted to a NoSQL database are rapidly moving to the cloud as they require more scalability and resilience. These are typically SQL database applications. So many developers who learned on document databases or other NoSQL databases now need to know SQL. Get started! Choose a SQL database Pick your favorite SQL database. If you pick a PostgreSQL-compatible database such as YugabyteDB the code samples should work without modification. If you pick MariaDB or another MySQL derivative, then you will probably have to change the data types and make minor modifications. The same can be said for Oracle Database or SQL Server. While SQL is a standard there are differences between the underlying database implementations and minor dialect choices that can be non-ecumenical. Regardless of your RDBMS choice, install it and get it running! A note on file handles This is not related to SQL, but really if you install any database you need to increase the file handle limits on your OS. For MacOS you can follow the instructions below. For Linux please consult your distribution’s documentation. Run launchctl limit maxfiles And check to see if the limit is a high enough number. I recommend 1048576 for most databases. If not then edit /etc/sysctl.conf and include the following: kern.maxfiles=1048576 kern.maxproc=2500 kern.maxprocperuid=2500 kern.maxfilesperproc=1048576 Create or edit /Library/LaunchDaemons/limit.maxfiles.plist and insert the following: <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key> <string>limit.maxfiles</string> <key>ProgramArguments</key> <array> <string>launchctl</string> <string>limit</string> <string>maxfiles</string> <string>1048576</string> <string>1048576</string> </array> <key>RunAtLoad</key> <true/> <key>ServiceIPC</key> <false/> </dict> </plist> Run sudo launchctl unload -w /Library/LaunchDaemons/limit.maxfiles.plist sudo launchctl load -w /Library/LaunchDaemons/limit.maxfiles.plist In whatever terminal session is used to run the database this command needs to be run: ulimit -n 1048576 Northwind sample database In order to run some queries you need a database schema and some data. Grab the Northwind sample database: DDL script – defines the schema DML script – loads the data This is basically the same kind of demo database that came with Microsoft Access back in the day, and that Microsoft still provides for SQL Server: IDG The Northwind database contains products, customers, orders, and order details among other things. It should be intuitive to anyone who has seen a business run. To create and load the database on YugabyteDB type ./bin/ysqlsh from your install directory. On PostgreSQL type ./bin/psql. Then from this shell type: CREATE DATABASE northwind; c northwind i northwind_ddl.sql i northwind_data.sql This enters the shell, creates the database, and loads the data. Note the slash commands are specific to the shell of PostgreSQL and compatible database systems. They are not SQL. If you are not a command-line junkie like me you can also use a tool like pgAdmin for either database. IDG The us_states table In PostgreSQL-compatible databases, from the shell you type dt to list the tables in the database. The output should look be something like this: List of relations Schema | Name | Type | Owner --------+------------------------+-------+---------- public | categories | table | yugabyte public | customer_customer_demo | table | yugabyte public | customer_demographics | table | yugabyte public | customers | table | yugabyte public | employee_territories | table | yugabyte public | employees | table | yugabyte public | order_details | table | yugabyte public | orders | table | yugabyte public | products | table | yugabyte public | region | table | yugabyte public | shippers | table | yugabyte public | suppliers | table | yugabyte public | territories | table | yugabyte public | us_states | table | yugabyte To view a table definition you type d and the name of the table, e.g. d us_states. In MySQL derivatives the comand is <a href="https://dev.mysql.com/doc/refman/8.0/en/show-columns.html" rel="nofollow">show columns from us_states</a> and in Oracle Database it is <a href="https://docs.oracle.com/javadb/10.8.3.0/tools/rtoolsijcomrefdescribe.html" rel="nofollow">describe us_states</a>. Regardless of the database your output should be something like this: Table "public.us_states" Column | Type | Collation | Nullable | Default --------------+------------------------+-----------+----------+--------- state_id | smallint | | not null | state_name | character varying(100) | | | state_abbr | character varying(2) | | | state_region | character varying(50) | | | Indexes: "us_states_pkey" PRIMARY KEY, lsm (state_id HASH) SQL select statement From the shell type select * from us_states. You will see something like: select * from us_states; state_id | state_name | state_abbr | state_region ----------+----------------------+------------+-------------- 4 | Arkansas | AR | south 46 | Vermont | VT | east 29 | Nevada | NV | west 25 | Mississippi | MS | south 14 | Illinois | IL | midwest 23 | Michigan | MI | north 1 | Alabama | AL | south 47 | Virginia | VA | east 37 | Oklahoma | OK | midwest 13 | Idaho | ID | midwest 20 | Maine | ME | north 27 | Montana | MT | west 51 | Wyoming | WY | west 39 | Pennsylvania | PA | east 32 | New Mexico | NM | west 45 | Utah | UT | west 2 | Alaska | AK | north 7 | Connecticut | CT | east 11 | Georgia | GA | south select is what tells the database that we’re retrieving records. The * means all columns. The from clause specifies which tables should be used to retrieve records. In this case only one table is used, us_states. All SQL statements end with a semicolon. Here is the command dissected into its constituent parts: select * from us_states; select – retrieve records * – all columns from – list of tables us_states – the table selected ; – end of statement SQL projection with a where clause Try a projection and a condition. This query retrieves all of the state abbreviations from the us_states table where the region is south — my favorite part of the US. From the shell run: select state_abbr from us_states where state_region='south'; state_abbr ------------ AR MS AL GA WV LA KY FL MO (9 rows) This time instead of signifying all columns with *, the statement uses what is called a projection, which is just a list of columns. This query includes only the state_abbr field in the list. where is how a list of conditions is specified — only records matching these conditions will be included. In this query, the condition is state_region equals the string literal south, which is delineated by the single quotes. Here are the constituent parts: select state_abbr from us_states where state_region='south'; state_abbr – a field from the table where – query conditions state_region='south' – a condition, return all rows where state_region has the string literal value south. Make sure the query has ‘real’ single quotes. “Double quotes” are used for something else and the stylized or ‘smart quotes’ are not interpreted as the same character. The products table For some of the next steps, a different table is required. Take a look at the products table: d products Table "public.products" Column | Type | Collation | Nullable | Default -------------------+-----------------------+-----------+----------+--------- product_id | smallint | | not null | product_name | character varying(40) | | not null | supplier_id | smallint | | | category_id | smallint | | | quantity_per_unit | character varying(20) | | | unit_price | real | | | units_in_stock | smallint | | | units_on_order | smallint | | | reorder_level | smallint | | | discontinued | integer | | not null | Indexes: "products_pkey" PRIMARY KEY, lsm (product_id HASH) Foreign-key constraints: "products_category_id_fkey" FOREIGN KEY (category_id) REFERENCES categories(category_id) "products_supplier_id_fkey" FOREIGN KEY (supplier_id) REFERENCES suppliers(supplier_id) Referenced by: TABLE "order_details" CONSTRAINT "order_details_product_id_fkey" FOREIGN KEY (product_id) REFERENCES products(product_id) SQL conditions In order to do some inventory planning, let’s get the product_name, number of units in stock, and units on order from the products table where the product is discontinued. Try select product_name, units_in_stock, units_on_order from products where discontinued=1; product_name | units_in_stock | units_on_order -------------------------------+----------------+---------------- Thüringer Rostbratwurst | 0 | 0 Chai | 39 | 0 Chang | 17 | 40 Chef Anton's Gumbo Mix | 0 | 0 Guaraná Fantástica | 20 | 0 Perth Pasties | 0 | 0 Rössle Sauerkraut | 26 | 0 Mishi Kobe Niku | 29 | 0 Singaporean Hokkien Fried Mee | 26 | 0 Alice Mutton | 0 | 0 (10 rows) Next, let’s look at all the products that are discontinued but still have units on order. Why order a product that will no longer be carried? Unfortunately, Northwind Traders has discontinued carrying their Thai beer! select product_name, units_in_stock, units_on_order from products where discontinued=1 and units_on_order >0; product_name | units_in_stock | units_on_order --------------+----------------+---------------- Chang | 17 | 40 select product_name, units_in_stock, units_on_order from products where discontinued=1 and units_on_order !=0; This last query has two conditions now; they both must be true. The discontinued field must be equal to 1 and units_on_order must be greater than 0. There are other operators that could be used for the same result such as not equals and greater and less than. For example select product_name, units_in_stock, units_on_order from products where discontinued>0 and units_on_order !=0; Broken down this query is: select product_name, units_in_stock, units_on_order from products where discontinued=1 and units_on_order >0; discontinued=1 units_on_order >0 and – both conditions must be true Conditions can also use or instead of and. So, the query below will pull back any discontinued items as well as any items that are on order (regardless of whether they are discontinued): select product_name, units_in_stock, units_on_order, discontinued from products where discontinued=1 or units_on_order>0; product_name | units_in_stock | units_on_order | discontinued -------------------------------+----------------+----------------+-------------- Thüringer Rostbratwurst | 0 | 0 | 1 Outback Lager | 15 | 10 | 0 Scottish Longbreads | 6 | 10 | 0 Louisiana Hot Spiced Okra | 4 | 100 | 0 Chai | 39 | 0 | 1 Gravad lax | 11 | 50 | 0 Mascarpone Fabioli | 9 | 40 | 0 Longlife Tofu | 4 | 20 | 0 Rogede sild | 5 | 70 | 0 Chang | 17 | 40 | 1 Queso Cabrales | 22 | 30 | 0 Aniseed Syrup | 13 | 70 | 0 Wimmers gute Semmelknödel | 22 | 80 | 0 Chef Anton's Gumbo Mix | 0 | 0 | 1 Guaraná Fantástica | 20 | 0 | 1 … (26 rows) This is roughly the same as the and query but now there are more rows because the query includes both discontinued products and anything on order regardless of whether it is discontinued or not. Note that this is the same as doing two queries and joining them together (using the union operator) from a data perspective (though it might be less efficient). Broken down this query is: select product_name, units_in_stock, units_on_order, discontinued from products where discontinued=1 OR units_on_order>0; discontinued=1 units_on_order >0 or – either condition must be true This query produces the same result as select product_name from products where discontinued=1 union select product_name from products where units_on_order>0; The orders and order_details tables To go further look at the orders table: d orders Table "public.orders" Column | Type | Collation | Nullable | Default ------------------+-----------------------+-----------+----------+--------- order_id | smallint | | not null | customer_id | bpchar | | | employee_id | smallint | | | order_date | date | | | required_date | date | | | shipped_date | date | | | ship_via | smallint | | | freight | real | | | ship_name | character varying(40) | | | ship_address | character varying(60) | | | ship_city | character varying(15) | | | ship_region | character varying(15) | | | ship_postal_code | character varying(10) | | | ship_country | character varying(15) | | | Indexes: "orders_pkey" PRIMARY KEY, lsm (order_id HASH) Foreign-key constraints: "orders_customer_id_fkey" FOREIGN KEY (customer_id) REFERENCES customers(customer_id) "orders_employee_id_fkey" FOREIGN KEY (employee_id) REFERENCES employees(employee_id) "orders_ship_via_fkey" FOREIGN KEY (ship_via) REFERENCES shippers(shipper_id) Referenced by: TABLE "order_details" CONSTRAINT "order_details_order_id_fkey" FOREIGN KEY (order_id) REFERENCES orders(order_id) Also look at its related child table, the order_details table: d order_details Table "public.order_details" Column | Type | Collation | Nullable | Default ------------+----------+-----------+----------+--------- order_id | smallint | | not null | product_id | smallint | | not null | unit_price | real | | not null | quantity | smallint | | not null | discount | real | | not null | Indexes: "order_details_pkey" PRIMARY KEY, lsm (order_id HASH, product_id) Foreign-key constraints: "order_details_order_id_fkey" FOREIGN KEY (order_id) REFERENCES orders(order_id) "order_details_product_id_fkey" FOREIGN KEY (product_id) REFERENCES products(product_id) SQL inner join For a summary query, take some of the columns from orders, some from order_details, and make a projection, then join them together. However, there are a lot of rows in this result so include a condition specifying only orders shipped to Portland. select o.order_id, o.customer_id, o.order_date, d.product_id, d.unit_price, d.quantity from orders o inner join order_details d on o.order_id=d.order_id where ship_city='Portland'; order_id | customer_id | order_date | product_id | unit_price | quantity ----------+-------------+------------+------------+------------+---------- 10708 | THEBI | 1997-10-17 | 5 | 21.35 | 4 10708 | THEBI | 1997-10-17 | 36 | 19 | 5 10307 | LONEP | 1996-09-17 | 62 | 39.4 | 10 10307 | LONEP | 1996-09-17 | 68 | 10 | 3 10544 | LONEP | 1997-05-21 | 28 | 45.6 | 7 10544 | LONEP | 1997-05-21 | 67 | 14 | 7 10992 | THEBI | 1998-04-01 | 72 | 34.8 | 2 10805 | THEBI | 1997-12-30 | 34 | 14 | 10 10805 | THEBI | 1997-12-30 | 38 | 263.5 | 10 11018 | LONEP | 1998-04-13 | 12 | 38 | 20 11018 | LONEP | 1998-04-13 | 18 | 62.5 | 10 11018 | LONEP | 1998-04-13 | 56 | 38 | 5 10662 | LONEP | 1997-09-09 | 68 | 12.5 | 10 10665 | LONEP | 1997-09-11 | 51 | 53 | 20 10665 | LONEP | 1997-09-11 | 59 | 55 | 1 10665 | LONEP | 1997-09-11 | 76 | 18 | 10 10883 | LONEP | 1998-02-12 | 24 | 4.5 | 8 10317 | LONEP | 1996-09-30 | 1 | 14.4 | 20 10310 | THEBI | 1996-09-20 | 16 | 13.9 | 10 10310 | THEBI | 1996-09-20 | 62 | 39.4 | 5 10867 | LONEP | 1998-02-03 | 53 | 32.8 | 3 (21 rows) The inner join matches on the order_id column so every order ID that has one or more matching order IDs in order_details is included in the result set. Additionally, the query specifies “aliases” for the tables, so in the from clause the order table is aliased to o. In the inner join clause, the order_id columns from both tables are matched using the on subclause. Broken down this is: select o.order_id, o.customer_id, o.order_date, d.product_id, d.unit_price, d.quantity from orders o inner join order_details d on o.order_id=d.order_id where ship_city='Portland'; o.order_id, d.product_id – order_id comes from the orders table, product_id comes from order_details. o and d are aliases. from orders o – in the from clause alias orders to o inner join order_details d – join order_details and alias it as d on o.order_id=d.order_id – the field used to join on is the order_id “foreign key” In the orders table, the order_id is guaranteed unique and it is called a primary key. In the order_details table it is a foreign key. When the same field in each table has the same value to match the records up it is called a “foreign key.” Essentially, an inner join is an intersection or that part in the middle of the Venn diagram. This query explicitly used an inner join clause, which I strongly recommend. However, some queries have two tables in the from clause and the where clause and they match the columns as an “implicit” join. It is not a big problem if there are two tables, but where clauses can get long. This can make finding the joins vs. conditions difficult, and having joins just randomly mixed with conditions is bad hygiene. However, when I learned SQL this was how you did an inner join. select o.order_id, o.customer_id, o.order_date, d.product_id, d.unit_price, d.quantity from orders o, order_details d where o.order_id=d.order_id and ship_city='Portland'; This query returns the same result as the previous query. SQL group by and sum Next, we’ll do a group by — joining the orders table with the order_details table, but grouping by the order_id from the orders table. Here, we’re just trying to find how much revenue Northwind will receive per order! select o.order_id, sum(d.unit_price * d.quantity) from orders o inner join order_details d on o.order_id=d.order_id where ship_city='Portland' group by o.order_id; order_id | sum ----------+------------------ 10307 | 424.000015258789 10708 | 180.400001525879 10992 | 69.5999984741211 11018 | 1575 10665 | 1295 10867 | 98.3999977111816 10317 | 287.999992370605 10544 | 417.199989318848 10662 | 125 10310 | 336.000003814697 10805 | 2775 10883 | 36 (12 rows) The sum function adds up all the columns for a “group.” In this case, the query also uses a mathematical operator to multiply price and quantity. The group by clause basically says, for each order, treat all of its line items in the order_details table as a group. Broken down this is: select o.order_id, sum(d.unit_price * d.quantity) from orders o or inner join order_details d on o.order_id=d.order_id where ship_city='Portland' group by o.order_id; sum(d.unit_price * d.quantity) – do unit_price * quantity on each row, and sum the results for the “group” group by o.order_id; – for every unique o.order_id, group them together for calculations like sum In this case for every order treat its line items as a “group,” so sum the totals for each order SQL group by and count In addition to sum there are other functions. For instance, you can count the line items. This query counts the order_details side of the join. select o.order_id, count(d.order_id) from orders o inner join order_details d on o.order_id=d.order_id where ship_city='Portland' group by o.order_id; order_id | count ----------+------- 10307 | 2 10708 | 2 10992 | 1 11018 | 3 10665 | 3 10867 | 1 10317 | 1 10544 | 2 10662 | 1 10310 | 2 10805 | 2 10883 | 1 (12 rows) You can validate the number by doing a select on one of the rows. So if you run a select query on the first order 10307 you will see that there are indeed two order_details records for the order. northwind=# select * from order_details where order_id=10307; order_id | product_id | unit_price | quantity | discount ----------+------------+------------+----------+---------- 10307 | 62 | 39.4 | 10 | 0 10307 | 68 | 10 | 3 | 0 (2 rows) In addition to using count for a group by, you can count all of the rows in a table by just doing select count(1) or select count(*) from a table. Broken down this query is: select o.order_id, count(d.order_id) from orders o inner join order_details d on o.order_id=d.order_id where ship_city='Portland' group by o.order_id; count(d.order_id) – count the d.order_ids in the o.order_id groups (in essence count the line items) Could have used a different field Could also do select count(*) from orders; to get row count for the table Could also do select count(1) from orders; (which will not be null) Does not count nulls SQL insert statement Before we insert a new product, let’s look at an existing product. Find some tofu. select * from products where product_name='Tofu'; product_id | product_name | supplier_id | category_id | quantity_per_unit | unit_price | units_in_stock | units_on_order | reorder_level | discontinued ------------+--------------+-------------+-------------+-------------------+------------+----------------+----------------+---------------+-------------- 14 | Tofu | 6 | 7 | 40 - 100 g pkgs. | 23.25 | 35 | 0 | 0 | 0 (1 row) Now let’s get the max product_id so that we don’t end up duplicating a primary key (product_id) in the new record. select max(product_id) from products; ----- 77 (1 row) Now based on the earlier row, we’ll insert a new product, Extra Firm Tofu, with a product_id of 78. insert into products values(78,'Extra Firm Tofu',6,7,'14 oz', 1.99, 50, 0, 0, 0); INSERT 0 1 Now if we select from tofu where product_id=78 we should see the new row: select * from products where product_id=78; product_id | product_name | supplier_id | category_id | quantity_per_unit | unit_price | units_in_stock | units_on_order | reorder_level | discontinued ------------+-----------------+-------------+-------------+-------------------+------------+----------------+----------------+---------------+-------------- 78 | Extra Firm Tofu | 6 | 7 | 14 oz | 1.99 | 50 | 0 | 0 | 0 (1 row) Again, doing the select from products first was just so that you do not have to look up categories or any of the other field values, but can just use an existing Tofu record is a template. Looking up the max(product_id) was so that you could add one to it and generate a new primary key. This is not a “safe” way to do this in a system with multiple users, but that is a much longer topic. The insert statement we used here is the simplest form, just using the values clause and setting the column values in order. However the “correct” or cleaner way is to include the field names in a separate clause. This is safer especially if the table definition changes. These concerns are irrelevant in a simple dataset with one user. In other words: select * from products where product_name='Tofu'; In order to avoid trying to figure out what product category or supplier to pick, just select an existing row to use as a template. select max(product_id) from products; The product_id is the primary key. It cannot be a duplicate, so just increment it. Danger – this is not the way to generate a primary key in production code (a topic for another day). insert into products values(78,'Extra Firm Tofu',6,7,'14 oz', 1.99, 50, 0, 0, 0); Inserted in order… but this is bad hygiene. insert into products (product_id,product_name,supplier_id,category_id,quantity_per_unit,unit_price,units_in_stock,units_on_order,reorder_level,discontinued) values(78,'Extra Firm Tofu',6,7,'14 oz', 1.99, 50, 0, 0, 0); This is the “right” way. If not included the column will be “null.” SQL left outer join In addition to inner joins, there are outer joins. Perhaps you want a list of the products whether or not they are included in an order, and you want to count how many orders there are for a given product. For example, it so happens that 33 orders contain Pâté chinoise, but no orders have Extra Firm Tofu. select p.product_name, count(d.order_id) from products p left outer join order_details d on p.product_id=d.product_id group by p.product_name; product_name | count ----------------------------------+------- Pâté chinois | 33 Camembert Pierrot | 51 Inlagd Sill | 31 Gustaf's Knäckebröd | 14 Thüringer Rostbratwurst | 32 Carnarvon Tigers | 27 Röd Kaviar | 14 Rhönbräu Klosterbier | 46 … Extra Firm Tofu | 0 … Gumbär Gummibärchen | 32 Original Frankfurter grüne Soße | 38 NuNuCa Nuß-Nougat-Creme | 18 Chang | 44 Manjimup Dried Apples | 39 Escargots de Bourgogne | 18 Chef Anton's Cajun Seasoning | 20 Ikura | 33 Uncle Bob's Organic Dried Pears | 29 (78 rows) A left outer join includes everything from one table, whether or not it occurs in the other table… However, it only includes the rows in the other table (B, orders) if they are in the first table (A, products). In this case the query includes all of the products, but only orders for those products. select p.product_name, count(d.order_id) from products p left outer join order_details d on p.product_id=d.product_id group by p.product_name; count(d.order_id) – count the number of orders for the product_name left outer join order_details d on p.product_id=d.product_id – join products against the order_details but still include products that have no records in order_details SQL update statement Finally, let’s change the name of the Extra Firm Tofu to Super Firm Tofu. update products set product_name='Super Firm Tofu' where product_id=78; The update statement just takes the table and a comma separated list of fields in the set clause. The where clause works essentially the same as in a select statement. SQL delete and drop Now let’s delete the tofu… Further, you may decide that you want to not only delete a table but remove its definition. For that you can drop it. (Do not do this if you do not want to reload everything before querying.) If you disconnect from the Northwind database… by connecting to a different database… you can drop the entire database. (Again, do not do this unless you want to start over completely.) For PostgreSQL-compatible databases, q quits. delete from products where product_id=78; DELETE 1 drop table us_states; DROP TABLE c postgres You are now connected to database "postgres" as user "yugabyte". postgres=# drop database northwind; DROP DATABASE q The where clause in the delete statement is indeed the same as used for the select statement. Deleting by primary key ensures just the single record is deleted. delete from products where product_id=78; – similar to a select statement but the row is deleted from the table drop table drops a whole table… drop table us_states; – does not just delete the rows but the whole table definition If you want to drop a database you must connect to a different database because you cannot drop the database you are connected to. This is not SQL but a ysqlsh or psql shell thing. drop database drops a whole database… drop database northwind; – drops not just the data but the table definitions c postgres – run this to connect to a different database (the default) because you cannot drop a database that you are connected to drop database northwind; – everything is gone… back to before you started What next? So many things… Functions AVG, MAX, MIN, and many more Joins Right outer join, full join, etc. Statements Update Many more Indexes Alter table Much much more. SQL tutorials and resources W3Schools – https://www.w3schools.com/sql/default.asp The PostgreSQL Tutorial – https://www.postgresqltutorial.com/ The Oracle Tutorial – https://www.oracletutorial.com/ The MySQL Tutorial – https://www.mysqltutorial.org/ Wikipedia on SQL Joins – https://en.wikipedia.org/wiki/Join_(SQL) SQL is a whole world SQL is not just a database query language but an ecosystem of tools and technologies. Everything from Excel to Tableau to Microsoft Access can connect to and query SQL databases or use them as a source. There are scores of people who cannot code and do not know JavaScript or the MongoDB Aggregation Pipeline but know SQL. The wonderful thing about SQL is that it is a very terse and simple language. There are many comparative queries that are a line or two of SQL that are many lines in a NoSQL database’s custom query language. There are even NoSQL databases, like Couchbase, that support a SQL dialect. And many GraphQL frameworks integrate with one or more SQL databases, usually through an object-relational mapping tool. So really, no developer is complete without at least a passing knowledge of the world’s most popular query language. SQLDatabasesSoftware DevelopmentAnalytics